elvish-0.21.0/000077500000000000000000000000001465720375400131015ustar00rootroot00000000000000elvish-0.21.0/.cirrus.yml000066400000000000000000000111101465720375400152030ustar00rootroot00000000000000test_arm_task: env: ELVISH_TEST_TIME_SCALE: "20" TEST_FLAG: -race name: Test on Linux ARM64 arm_container: # The Alpine image has segmentation faults when running test -race, so # use Debian instead. image: golang:1.22-bookworm go_version_script: go version test_script: go test $TEST_FLAG ./... test_bsd_task: env: ELVISH_TEST_TIME_SCALE: "20" TEST_FLAG: -race GO_VERSION: "1.22.0" PATH: /usr/local/go/bin:$PATH matrix: - name: Test on FreeBSD freebsd_instance: # Find latest version on https://www.freebsd.org/releases/ image_family: freebsd-14-0 setup_script: # go test -race is not compatible with ASLR, which has been enabled by # default since FreeBSD 13 # (https://wiki.freebsd.org/AddressSpaceLayoutRandomization). LLVM # issue: https://github.com/llvm/llvm-project/issues/53256 # # There's also a Go bug where using go test -race with ASLR fails # to run the tests and still reports tests as passing: # https://github.com/golang/go/issues/65425 sysctl kern.elf64.aslr.enable=0 - name: Test on NetBSD compute_engine_instance: image_project: pg-ci-images # Find latest version in the "VERSION:" variable for the NetBSD image in # https://github.com/anarazel/pg-vm-images/blob/main/.cirrus.yml image: family/pg-ci-netbsd-vanilla-9-3 platform: netbsd - name: Test on OpenBSD compute_engine_instance: image_project: pg-ci-images # Find latest version in the "VERSION:" variable for the OpenBSD image in # https://github.com/anarazel/pg-vm-images/blob/main/.cirrus.yml image: family/pg-ci-openbsd-vanilla-7-3 platform: openbsd go_toolchain_cache: fingerprint_key: $CIRRUS_OS-$GO_VERSION folder: /usr/local/go populate_script: | curl -L -o go.tar.gz https://go.dev/dl/go$GO_VERSION.$CIRRUS_OS-amd64.tar.gz mkdir -p /usr/local tar -C /usr/local -xzf go.tar.gz go_version_script: go version test_script: go test $TEST_FLAG ./... build_binaries_task: name: Build binaries only_if: $CIRRUS_BRANCH == 'master' alias: binaries env: CGO_ENABLED: "0" container: # Keep the Go version part in sync with # https://github.com/elves/up/blob/master/Dockerfile image: golang:1.22.0-alpine go_modules_cache: fingerprint_script: cat go.sum folder: ~/go/pkg/mod go_build_cache: folder: ~/.cache/go-build # Git is not required for building the binaries, but we need to include for Go # to include VCS information in the binary. Also install coreutils to get a # touch command that supports specifying the timezone. setup_script: apk add zip git coreutils # _bin is in .gitignore, so Git won't consider the repo dirty. This will # impact the binary, which encodes VCS information. build_binaries_script: | go run ./cmd/elvish ./tools/buildall.elv -name elvish-HEAD -variant official ./cmd/elvish _bin/ binaries_artifacts: path: _bin/** binary_checksums_artifacts: path: _bin/*/*.sha256sum check_binary_checksums_task: name: Check binary checksums ($HOST) only_if: $CIRRUS_BRANCH == 'master' container: image: alpine:latest depends_on: binaries matrix: - env: HOST: cdg - env: HOST: hkg setup_script: apk add git curl # Enable auto cancellation - if there is another push, only the task to # compare the website against the newer commit should continue. auto_cancellation: "true" wait_website_update_script: | ts=$(git show -s --format=%ct HEAD) wait=10 while true; do if website_ts=$(curl -sSf https://$HOST.elv.sh/commit-ts.txt); then if test "$website_ts" -ge "$ts"; then echo "website ($website_ts) >= CI ($ts)" exit 0 else echo "website ($website_ts) < CI ($ts)" fi else echo "website has no commit-ts.txt yet" fi sleep $wait test $wait -lt 96 && wait=`echo "$wait * 2" | bc` done check_binary_checksums_script: | curl -o checksums.zip https://api.cirrus-ci.com/v1/artifact/build/$CIRRUS_BUILD_ID/binaries/binary_checksums.zip unzip checksums.zip cd _bin ret=0 for f in */elvish-HEAD.sha256sum */elvish-HEAD.exe.sha256sum; do website_sum=$(curl -sS https://$HOST.dl.elv.sh/$f | awk '{print $1}') ci_sum=$(cat $f | awk '{print $1}') if test "$website_sum" = "$ci_sum"; then echo "$f: website == CI ($ci_sum)" else echo "$f: website ($website_sum) != CI ($ci_sum)" ret=1 fi done exit $ret elvish-0.21.0/.codecov.yml000066400000000000000000000013071465720375400153250ustar00rootroot00000000000000coverage: status: project: default: threshold: 0.1% patch: off comment: false # The following patterns are also consumed by a hacky sed script in # tools/prune-cover.sh, which does not support globs. ignore: # Exclude test helpers. - "pkg/cli/clitest" - "pkg/cli/histutil/test_db.go" - "pkg/eval/evaltest" - "pkg/eval/vals/tester.go" - "pkg/prog/progtest" - "pkg/store/storetest" - "pkg/must" # Exclude commands for manual testing. - "pkg/cli/examples" - "pkg/md/mdrun" # Exclude files generated by stringer. - "pkg/getopt/zstring.go" - "pkg/md/zstring.go" - "pkg/parse/zstring.go" # Exclude the copied diff and rpc packages. - "pkg/diff" - "pkg/rpc" elvish-0.21.0/.codespellrc000066400000000000000000000002411465720375400153760ustar00rootroot00000000000000[codespell] ignore-words-list = ro,upto,nd,doas,fo,shouldbe,iterm,lates,testof skip = ./.git,./vscode/node_modules,./vscode/dist,./website/_dst,./website/*.html elvish-0.21.0/.dockerignore000066400000000000000000000000041465720375400155470ustar00rootroot00000000000000/_* elvish-0.21.0/.gitattributes000066400000000000000000000000261465720375400157720ustar00rootroot00000000000000*.go filter=goimports elvish-0.21.0/.github/000077500000000000000000000000001465720375400144415ustar00rootroot00000000000000elvish-0.21.0/.github/FUNDING.yml000066400000000000000000000000331465720375400162520ustar00rootroot00000000000000github: xiaq patreon: xiaq elvish-0.21.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001465720375400166245ustar00rootroot00000000000000elvish-0.21.0/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000023321465720375400215170ustar00rootroot00000000000000name: Bug Report description: File a bug report body: - type: markdown attributes: value: | Thanks for taking the time to file a bug report. Here are some tips: - Please only use this form for bugs in Elvish. If you need help with using Elvish, the forum or chatroom (linked from the repo README) are more suitable places. - Please search existing issues to see if the same or similar report has been filed before. - type: textarea id: content attributes: label: What happened, and what did you expect to happen? validations: required: true - type: input id: version attributes: label: Output of "elvish -version" description: | The bug may have already been fixed. Whenever possible, please use either the latest release or the latest development build to see the bug still exists. You can still file an issue if you are running an old version and it's too hard to install a new version. validations: required: true - type: checkboxes id: terms attributes: label: Code of Conduct options: - label: I agree to follow Elvish's [Code of Conduct](https://src.elv.sh/CODE_OF_CONDUCT.md). required: true elvish-0.21.0/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000023271465720375400225560ustar00rootroot00000000000000name: Feature Request description: File a feature request body: - type: markdown attributes: value: | Thanks for taking the time to file a feature request. Here are some tips: - Please only use issues for feature requests for Elvish. If you need help with using Elvish, the forum or chatroom (linked from the repo README) is a more suitable place. - Please search existing issues to see if the same or similar report has been filed before. - type: textarea id: content attributes: label: What new feature should Elvish have? validations: required: true - type: input id: version attributes: label: Output of "elvish -version" description: | The feature may have already been added. Whenever possible, please use the latest development build to see if it has the feature you need. You can still file an issue if you are running an old version and it's too hard to install a new version. validations: required: true - type: checkboxes id: terms attributes: label: Code of Conduct options: - label: I agree to follow Elvish's [Code of Conduct](https://src.elv.sh/CODE_OF_CONDUCT.md). required: true elvish-0.21.0/.github/workflows/000077500000000000000000000000001465720375400164765ustar00rootroot00000000000000elvish-0.21.0/.github/workflows/check_cirrus.yml000066400000000000000000000017641465720375400216750ustar00rootroot00000000000000name: 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.21.0/.github/workflows/check_website.yml000066400000000000000000000057361465720375400220330ustar00rootroot00000000000000name: Check website on: push: branches: - master jobs: check_freshness: name: Check freshness if: github.repository == 'elves/elvish' runs-on: ubuntu-latest strategy: matrix: host: [cdg, hkg] steps: - name: Checkout code uses: actions/checkout@v4 - name: Compare timestamp timeout-minutes: 30 run: | ts=$(git show -s --format=%ct HEAD) wait=10 while true; do if website_ts=$(curl -sSf https://${{ matrix.host }}.elv.sh/commit-ts.txt); then if test "$website_ts" -ge "$ts"; then echo "website ($website_ts) >= current ($ts)" exit 0 else echo "website ($website_ts) < current ($ts)" fi else echo "website has no commit-ts.txt yet" fi sleep $wait test $wait -lt 96 && wait=`echo "$wait * 2" | bc` done build_binaries: name: Build binaries runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: # Keep this in sync with # https://github.com/elves/up/blob/master/Dockerfile go-version: 1.22.0 - name: Build binaries run: go run ./cmd/elvish ./tools/buildall.elv -name elvish-HEAD -variant official ./cmd/elvish ~/elvish-bin/ - name: Upload binaries uses: actions/upload-artifact@v4 with: name: bin path: ~/elvish-bin/**/* retention-days: 7 - name: Upload binary checksums uses: actions/upload-artifact@v4 with: name: bin-checksums path: ~/elvish-bin/*/*.sha256sum check_binary_checksums: name: Check binary checksums needs: [check_freshness, build_binaries] strategy: matrix: host: [cdg, hkg] runs-on: ubuntu-latest steps: - name: Download binary checksums uses: actions/download-artifact@v4 with: name: bin-checksums path: elvish-bin - name: Check binary checksums working-directory: elvish-bin run: | ret=0 for f in */elvish-HEAD.sha256sum */elvish-HEAD.exe.sha256sum; do website_sum=$(curl -sS https://${{ matrix.host }}.dl.elv.sh/$f | awk '{print $1}') github_sum=$(cat $f | awk '{print $1}') if test "$website_sum" = "$github_sum"; then echo "$f: website == github ($github_sum)" else echo "$f: website ($website_sum) != github ($github_sum)" ret=1 fi done if test $ret != 0; then latest_sha=$(curl -sS -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H 'Accept: application/vnd.github.VERSION.sha' https://api.github.com/repos/elves/elvish/commits/master) if test ${{ github.sha }} != "$latest_sha"; then echo "Ignoring the mismatch since there is a newer commit now" ret=0 fi fi exit $ret elvish-0.21.0/.github/workflows/ci.yml000066400000000000000000000102651465720375400176200ustar00rootroot00000000000000name: 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: # The default name will include the "go-version-is" parameter, whcih is # derived from go-version and redundant, so we supply an explicit templated # name. name: Run tests (${{ matrix.os }}, ${{ matrix.go-version }}) strategy: matrix: os: [ubuntu, macos, windows] go-version: [1.22.x] go-version-is: [new] include: # Test old supported Go version - os: ubuntu go-version: 1.21.x go-version-is: [old] env: ELVISH_TEST_TIME_SCALE: 20 runs-on: ${{ matrix.os }}-latest steps: # autocrlf is problematic for fuzz testdata. - name: Turn off autocrlf if: matrix.os == 'windows' run: git config --global core.autocrlf false - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Test with race detection run: | go test -race ./... cd website; go test -race ./... - name: Generate test coverage if: matrix.go-version-is == 'new' run: go test -coverprofile=cover -coverpkg=./pkg/... ./pkg/... - name: Save test coverage if: matrix.go-version-is == 'new' uses: actions/upload-artifact@v4 with: name: cover-${{ matrix.os == 'ubuntu' && 'linux' || matrix.os }} 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@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.22.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@v4 - name: Download test coverage uses: actions/download-artifact@v4 with: name: cover-${{ matrix.ostype }} - name: Upload coverage to codecov uses: codecov/codecov-action@v3 with: files: ./cover flags: ${{ matrix.ostype }} checks: name: Run checks runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.22.x - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install tools run: | go install golang.org/x/tools/cmd/stringer@latest go install golang.org/x/tools/cmd/goimports@latest # Keep the versions of staticcheck and codespell in sync with CONTRIBUTING.md go install honnef.co/go/tools/cmd/staticcheck@v0.4.6 pip install --user codespell==2.2.6 - name: Run checks run: make all-checks check-rellinks: name: Check relative links in website runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.22.x - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install Python dependency run: pip3 install beautifulsoup4 - name: Check relative links run: make -C website check-rellinks 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@v4 - 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.21.0/.github/workflows/docker.yml000066400000000000000000000015041465720375400204700ustar00rootroot00000000000000name: Docker on: push: jobs: docker: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} elvish-0.21.0/.gitignore000066400000000000000000000004421465720375400150710ustar00rootroot00000000000000# 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 # Project specific cover /_* /elvish elvish-0.21.0/.vscode/000077500000000000000000000000001465720375400144425ustar00rootroot00000000000000elvish-0.21.0/.vscode/launch.json000066400000000000000000000003201465720375400166020ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "Run Extension", "type": "extensionHost", "request": "launch", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], } ] } elvish-0.21.0/0.21.0-release-notes.md000066400000000000000000000057171465720375400167170ustar00rootroot00000000000000# Notable new features - A new [`with`](../ref/language.html#with) command for running a lambda with temporary assignments. - A new [`keep-if`](../ref/builtin.html#keep-if) command. - The [`os`](../ref/os.html) module has gained the following new commands: `mkdir-all`, `symlink` and `rename`. - A new [`render-styledown`](../ref/builtin.html#render-styledown) command. - A new [`str:repeat`](../ref/str.html#str:repeat) command. - A new [`md`](../ref/md.html) module, currently containing a single function `md:show` for rendering Markdown in the terminal. - On Unix, Elvish now turns off output flow control (IXON) by default, freeing up Ctrl-S and Ctrl-Q for keybindings. Users who require this feature can turn it back on by running `stty ixon`. # Notable bugfixes - The string comparison commands `s` and `>=s` (but not `!=s`) now accept any number of arguments, as they are documented to do. - Temporary assignments now work correctly on map and list elements ([#1515](https://b.elv.sh/1515)). - The terminal line editor is now more aggressive in suppressing compilation errors caused by the code not being complete. For example, during the process of typing out `echo $pid`, the editor no longer complains that `$p` is undefined when the user has typed `echo $p`. # Deprecations - The implicit cd feature is now deprecated. Use `cd` or location mode instead. # Breaking changes - The `eawk` command, deprecated since 0.20.0, has been removed. Use [`re:awk`](../ref/re.html#re:awk) instead. - Support for the legacy `~/.elvish` directory, deprecated since 0.16.0, has been removed. For the supported directory paths, see documentation for [the Elvish command](../ref/command.html). - Support for the legacy temporary assignment syntax (`a=b command`), deprecated since 0.18.0, has been removed. Use either the [`tmp`](../ref/language.html#tmp) command (available since 0.18.0) or the [`with`](../ref/language.html#with) command (available since this release) instead. - The commands `!=`, `!=s` and `not-eq` now only accepts two arguments ([#1767](https://b.elv.sh/1767)). - The commands `edit:kill-left-alnum-word` and `edit:kill-right-alnum-word` have been renamed to `edit:kill-alnum-word-left` and `edit:kill-alnum-word-right`, to be consistent with the documentation and the names of other similar commands. If you need to write code that supports both names, use `has-key` to detect which name is available: ```elvish fn kill-alnum-word-left { if (has-key edit: kill-alnum-word-left~) { edit:kill-alnum-word-left } else { edit:kill-left-alnum-word } } ``` - Using `else` without `catch` in the `try` special command is no longer supported. The command `try { a } else { b } finally { c }` is equivalent to just `try { a; b } finally { c }`. elvish-0.21.0/CODE_OF_CONDUCT.md000066400000000000000000000013431465720375400157010ustar00rootroot00000000000000The Elvish community follows the [Go community code of conduct](https://go.dev/conduct). The short version: - Treat everyone with respect and kindness. - Be thoughtful in how you communicate. - Don't be destructive or inflammatory. - If you encounter an issue, please contact xiaq via Telegram, Matrix or Discord DM or email (xiaqqaix@gmail.com). (We don't have a team of stewards or a committee of representatives (yet).) Consistent with the Go community code of conduct, the following specific points apply: - Respect how other people choose to use Elvish and their system in general. Elvish is opinionated software, but that doesn't mean the use cases Elvish chooses not to support are inherently bad. elvish-0.21.0/Dockerfile000066400000000000000000000003751465720375400151000ustar00rootroot00000000000000FROM golang:1.22-alpine3.19 as builder RUN apk add --no-cache --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.19 COPY --from=builder /go/bin/elvish /bin/elvish CMD ["/bin/elvish"] elvish-0.21.0/LICENSE000066400000000000000000000024241465720375400141100ustar00rootroot00000000000000Copyright (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.21.0/Makefile000066400000000000000000000030601465720375400145400ustar00rootroot00000000000000ELVISH_MAKE_BIN ?= $(or $(GOBIN),$(shell go env GOPATH)/bin)/elvish$(shell go env GOEXE) ELVISH_MAKE_BIN := $(subst \,/,$(ELVISH_MAKE_BIN)) ELVISH_MAKE_PKG ?= ./cmd/elvish default: test most-checks get # This target emulates the behavior of "go install ./cmd/elvish", except that # the build output and the main package to build can be overridden with # environment variables. get: mkdir -p $(shell dirname $(ELVISH_MAKE_BIN)) go build -o $(ELVISH_MAKE_BIN) $(ELVISH_MAKE_PKG) # Run formatters on Go and Markdown files. fmt: find . -name '*.go' | xargs goimports -w find . -name '*.go' | xargs gofmt -s -w find . -name '*.md' | xargs go run src.elv.sh/cmd/elvmdfmt -w -width 80 # Run unit tests, with race detection if the platform supports it. test: go test $(shell ./tools/run-race.elv) ./... cd website; go test $(shell ./tools/run-race.elv) ./... # Generate a basic test coverage report, and open it in the browser. The report # is an approximation of https://app.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 }' # All the checks except check-gen.sh, which is not always convenient to run as # it requires a clean working tree. most-checks: ./tools/check-fmt-go.sh ./tools/check-fmt-md.sh ./tools/check-disallowed.sh codespell go vet ./... staticcheck ./... all-checks: most-checks ./tools/check-gen.sh .PHONY: default get fmt test cover most-checks all-checks elvish-0.21.0/README.md000066400000000000000000000067761465720375400144000ustar00rootroot00000000000000# Elvish [![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/tree/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) [![Forum](https://img.shields.io/badge/forum-bbs.elv.sh-5b5.svg?logo=discourse)](https://bbs.elv.sh) [![Twitter](https://img.shields.io/badge/twitter-@ElvishShell-blue.svg?logo=x)](https://twitter.com/ElvishShell) [![Telegram Group](https://img.shields.io/badge/telegram-Elvish-blue.svg?logo=telegram&logoColor=white)](https://t.me/+Pv5ZYgTXD-YaKwcP) [![Discord server](https://img.shields.io/badge/discord-Elvish-blue.svg?logo=discord&logoColor=white)](https://discord.gg/jrmuzRBU8D) [![#users:elv.sh](https://img.shields.io/badge/matrix-%23users:elv.sh-blue.svg?logo=matrix)](https://matrix.to/#/#users:elv.sh) [![#elvish on libera.chat](https://img.shields.io/badge/libera.chat-%23elvish-blue.svg?logo=liberadotchat&logoColor=white)](https://web.libera.chat/#elvish) [![Gitter](https://img.shields.io/badge/gitter-elves%2Felvish-blue.svg?logo=gitter)](https://gitter.im/elves/elvish) (Chat rooms are all bridged together thanks to [Matrix](https://matrix.org).) Elvish is: - A powerful scripting language. - A shell with useful interactive features built-in. - A statically linked binary for Linux, BSDs, macOS or Windows. Elvish is pre-1.0. This means that breaking changes will still happen from time to time, but it's stable enough for both scripting and interactive use. ## Documentation [![User docs](https://img.shields.io/badge/User_Docs-37a779?style=for-the-badge)](https://elv.sh) User docs are hosted on Elvish's website, [elv.sh](https://elv.sh). This includes [how to install Elvish](https://elv.sh/get/), [tutorials](https://elv.sh/learn/), [reference pages](https://elv.sh/ref/), and [news](https://elv.sh/blog/). [![Development docs](https://img.shields.io/badge/Development_Docs-blue?style=for-the-badge)](./docs) Development docs are in [./docs](./docs). [![Awesome Elvish](https://img.shields.io/badge/Awesome_Elvish-orange?style=for-the-badge)](https://github.com/elves/awesome-elvish) Awesome Elvish packages and tools that support Elvish. ## License All source files use the BSD 2-clause license (see [LICENSE](LICENSE)), except for the following: - Files in [pkg/diff](pkg/diff) and [pkg/rpc](pkg/rpc) are released under the BSD 3-clause license, since they are derived from [Go's source code](https://github.com/golang/go). See [pkg/diff/LICENSE](pkg/diff/LICENSE) and [pkg/rpc/LICENSE](pkg/rpc/LICENSE). - Files in [pkg/persistent](pkg/persistent) and its subdirectories are released under EPL 1.0, since they are partially derived from [Clojure's source code](https://github.com/clojure/clojure). See [pkg/persistent/LICENSE](pkg/persistent/LICENSE). - Files in [pkg/md/spec](pkg/md/spec) are released under the Creative Commons CC-BY-SA 4.0 license, since they are derived from [the CommonMark spec](https://github.com/commonmark/commonmark-spec). See [pkg/md/spec/LICENSE](pkg/md/spec/LICENSE). elvish-0.21.0/branding/000077500000000000000000000000001465720375400146655ustar00rootroot00000000000000elvish-0.21.0/branding/forum-banner-dark.svg000066400000000000000000000075671465720375400207370ustar00rootroot00000000000000 λ bbs.elv elvish-0.21.0/branding/forum-banner-light.svg000066400000000000000000000075711465720375400211200ustar00rootroot00000000000000 λ bbs.elv elvish-0.21.0/branding/forum-logo.svg000066400000000000000000000060241465720375400174760ustar00rootroot00000000000000 λ elvish-0.21.0/branding/logo-full-bleed.svg000066400000000000000000000050251465720375400203610ustar00rootroot00000000000000 λ elvish-0.21.0/branding/logo.svg000066400000000000000000000047611465720375400163560ustar00rootroot00000000000000 λ elvish-0.21.0/cmd/000077500000000000000000000000001465720375400136445ustar00rootroot00000000000000elvish-0.21.0/cmd/elvish/000077500000000000000000000000001465720375400151365ustar00rootroot00000000000000elvish-0.21.0/cmd/elvish/main.go000066400000000000000000000012531465720375400164120ustar00rootroot00000000000000// 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/lsp" "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{}, &lsp.Program{}, &shell.Program{ActivateDaemon: daemon.Activate}))) } elvish-0.21.0/cmd/elvmdfmt/000077500000000000000000000000001465720375400154625ustar00rootroot00000000000000elvish-0.21.0/cmd/elvmdfmt/main.go000066400000000000000000000042101465720375400167320ustar00rootroot00000000000000// Command elvmdfmt reformats Markdown sources. // // This command is used to reformat all Markdown files in this repo; see the // [contributor's manual] on how to use it. // // For general information about the Markdown implementation used by this // command, see [src.elv.sh/pkg/md]. // // [contributor's manual]: https://github.com/elves/elvish/blob/master/CONTRIBUTING.md#formatting package main import ( "flag" "fmt" "html" "io" "os" "src.elv.sh/pkg/diff" "src.elv.sh/pkg/md" ) var ( overwrite = flag.Bool("w", false, "write result to source file (requires -fmt)") showDiff = flag.Bool("d", false, "show diff") width = flag.Int("width", 0, "if > 0, reflow content to width") ) func main() { md.UnescapeHTML = html.UnescapeString flag.Parse() files := flag.Args() if len(files) == 0 { text, err := io.ReadAll(os.Stdin) handleReadError("stdin", err) result, unsupported := format(string(text)) fmt.Print(result) handleUnsupported("stdin", unsupported) return } for _, file := range files { textBytes, err := os.ReadFile(file) handleReadError(file, err) text := string(textBytes) result, unsupported := format(text) handleUnsupported(file, unsupported) if *overwrite { err := os.WriteFile(file, []byte(result), 0644) if err != nil { fmt.Fprintf(os.Stderr, "write %s: %v\n", file, err) os.Exit(2) } } else if !*showDiff { fmt.Print(result) } if *showDiff { os.Stdout.Write(diff.Diff(file+".orig", text, file, result)) } } } func format(original string) (string, *md.FmtUnsupported) { codec := &md.FmtCodec{Width: *width} formatted := md.RenderString(original, codec) return formatted, codec.Unsupported() } func handleReadError(name string, err error) { if err != nil { fmt.Fprintf(os.Stderr, "read %s: %v\n", name, err) os.Exit(2) } } func handleUnsupported(name string, u *md.FmtUnsupported) { if u == nil { return } if u.NestedEmphasisOrStrongEmphasis { fmt.Fprintln(os.Stderr, name, "contains nested emphasis or strong emphasis") } if u.ConsecutiveEmphasisOrStrongEmphasis { fmt.Fprintln(os.Stderr, name, "contains consecutive emphasis or strong emphasis") } os.Exit(2) } elvish-0.21.0/cmd/nodaemon/000077500000000000000000000000001465720375400154445ustar00rootroot00000000000000elvish-0.21.0/cmd/nodaemon/elvish/000077500000000000000000000000001465720375400167365ustar00rootroot00000000000000elvish-0.21.0/cmd/nodaemon/elvish/main.go000066400000000000000000000006231465720375400202120ustar00rootroot00000000000000// Command elvish is an alternative main program of Elvish that does not include // the daemon subprogram. package main import ( "os" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/lsp" "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{}, &lsp.Program{}, &shell.Program{}))) } elvish-0.21.0/cmd/withpprof/000077500000000000000000000000001465720375400156665ustar00rootroot00000000000000elvish-0.21.0/cmd/withpprof/elvish/000077500000000000000000000000001465720375400171605ustar00rootroot00000000000000elvish-0.21.0/cmd/withpprof/elvish/main.go000066400000000000000000000010101465720375400204230ustar00rootroot00000000000000// Command elvish is an alternative main program of Elvish that supports writing // pprof profiles. package main import ( "os" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/lsp" "src.elv.sh/pkg/pprof" "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( &pprof.Program{}, &buildinfo.Program{}, &daemon.Program{}, &lsp.Program{}, &shell.Program{ActivateDaemon: daemon.Activate}))) } elvish-0.21.0/docs/000077500000000000000000000000001465720375400140315ustar00rootroot00000000000000elvish-0.21.0/docs/README.md000066400000000000000000000015511465720375400153120ustar00rootroot00000000000000💡 Tip: If you are looking for docs for Elvish users, like tutorials and reference pages, refer to Elvish's website [elv.sh](https://elv.sh) instead. This directory contains developer documentation: - 🏗️ [Building Elvish from source](building.md) - 📦 [Packaging Elvish](packaging.md) - 🔑 [Security policy](security.md) - 🧩 [Using Elvish as a library](elvish-as-library.md) If you'd like to contribute to Elvish: - 🏢 [Architecture overview](https://pkg.go.dev/src.elv.sh@master/docs/architecture) This document is written as a godoc comment. You can also read the Go source [architecture/doc.go](architecture/doc.go). - 👋 [Process for contributing to Elvish](contributing.md) - 🧪 [Testing changes](testing.md) - 📚 [Documenting changes](documenting.md) - 🔧 [Common development workflows](workflows.md) elvish-0.21.0/docs/architecture/000077500000000000000000000000001465720375400165135ustar00rootroot00000000000000elvish-0.21.0/docs/architecture/doc.go000066400000000000000000000153061465720375400176140ustar00rootroot00000000000000/* # Overview This file documents how Elvish's codebase is structured on a high level. You can read it either in a code editor, or in a godoc viewer such as https://pkg.go.dev/src.elv.sh@master/docs/architecture. Elvish is a Go project. If you are not familiar with how Go code is organized, start with [how to write Go code]. Go code in the Elvish repo lives under two directories: - The cmd directory contains Elvish's entrypoints, but it contains very little code. - The pkg directory has most of Elvish's Go code. It has a lot of subdirectories, so it can be a bit hard to find your bearing just by exploring the file tree. We will cover the cmd directory first, and then focus on the most important subdirectories under pkg. The Elvish repo also contains other directories. They are not technically part of the Go program, so we won't cover them here. Read their respective README files to learn more. # Module, package and symbol names Elvish's module name is [src.elv.sh]. You can think of it as an alias to where the code is actually hosted (currently [github.com/elves/elvish]). The import paths of all the packages start with the module name [src.elv.sh]. For example, the import path of the package in pkg/parse is [src.elv.sh/pkg/parse]. When referring to a symbol from a package, we'll use just the last component of the package's import path. For example, the Evaler type from the [src.elv.sh/pkg/eval] package is simply [eval.Evaler]. (This is consistent with Go's syntax.) # Entrypoints (cmd/elvish and pkg/prog) The default entrypoint of Elvish is [src.elv.sh/cmd/elvish]. It has a main function that does the following: - Assemble a "composite program" from multiple subprograms, most notably [shell.Program]. - Call [prog.Run]. You can read about the advantage of this approach in the godoc of [src.elv.sh/pkg/prog]. There are other main packages, like [src.elv.sh/cmd/withpprof/elvish]. They follow the same structure and only differ in which subprograms they include. # The shell subprogram (pkg/shell) The shell subprogram has two slightly different "modes", interactive and non-interactive, depending on the command-line arguments. The doc for [shell.Program] contains more details. In both modes, the shell subprogram uses the interpreter implemented in [src.elv.sh/pkg/eval] to evaluate code. In interactive mode, the shell also uses the line editor implemented in [src.elv.sh/pkg/edit] to read commands interactively. Some features of the editor depend on persistent storage; the shell subprogram also takes care of initializing that, using [src.elv.sh/pkg/daemon]. # The interpreter (pkg/eval) The [src.elv.sh/pkg/eval] package is perhaps the most important package in Elvish, as it implements the Elvish language and the builtin module. The interpreter is represented by [eval.Evaler], which is created with [eval.NewEvaler]. The method [eval.Evaler.Eval] (yes, that's 3 "evals"s) evaluates Elvish code, and does so in several steps: 1. Invoke the parser to get an AST. 2. Compile the AST into an "operation tree". 3. Run the operation tree. This approach is chosen mainly for its simplicity. It's probably not very performant. The compilation of each AST node into its corresponding operation node, as well as how each operation node runs, is defined in the several compile_*.go files. These files are where most of the language semantics is implemented. Another sizable chunk of this package is the various builtin_fn_*.go files, which implement functions of the builtin module. These may be moved to a different package in future. Some other packages important for the interpreter are: - [src.elv.sh/pkg/eval/vals] implements a standard set of operations for Elvish values. - [src.elv.sh/pkg/persistent] implements Elvish's lists and maps, modeled after [Clojure's vectors and maps]. - Subdirectories of [src.elv.sh/pkg/mods] implement the various builtin modules. # The parser (pkg/parse) The [src.elv.sh/pkg/parse] package implements parsing of Elvish code, with the [parse.Parse] as the entrypoint. The parsing algorithm is a handwritten [recursive descent] one, with the slightly unusual property that there's no separate tokenization phase. Read the package's godoc for more details. # The editor (pkg/edit) The [src.elv.sh/pkg/edit] package contains Elvish's interactive line editor, represented by [edit.Editor]. The traditional term "line editor" is a bit of a misnomer; modern line editors (including Elvish's) are similar to full-blown TUI applications like Vim, except that they usually restrict themselves to the last N lines of the terminal rather than the entire screen. The editor is built on top of the more low-level [src.elv.sh/pkg/cli] package (which is also a bit of a misnomer), in particular the [cli.App] type. The entire TUI stack is due for a rewrite soon. The editor relies on persistent storage for features like the directory history and the command history. As mentioned above, the initialization of the storage is done in pkg/shell, using pkg/daemon. # The storage daemon (pkg/daemon) Support for persistent storage is is currently provided by a storage daemon. The [src.elv.sh/pkg/daemon] packages implements two things: - A subprogram implementing the storage daemon ([daemon.Program]). - A client to talk to the daemon (returned by [daemon.Activate]). The daemon is launched and terminated on demand: - The first interactive Elvish shell launches the daemon. - Subsequent interactive shells talk to the same daemon. - When the last interactive Elvish shell quits, the daemon also quits. Internally, the daemon uses [bbolt] as the database engine. In future (subject to evaluation) Elvish might get a custom database, and the daemon might go away. # Closing remarks This should have given you a rough idea of the most important bits of Elvish's implementation. The implementation prioritizes readability, and most exported symbols are documented, so feel free to dive into the source code! If you have questions, feel free to ask in the user group or DM xiaq. [how to write Go code]: https://go.dev/doc/code [github.com/elves/elvish]: https://github.com/elves/elvish [src.elv.sh]: https://src.elv.sh [Clojure's vectors and maps]: https://clojure.org/reference/data_structures [recursive descent]: https://en.wikipedia.org/wiki/Recursive_descent_parser [bbolt]: https://github.com/etcd-io/bbolt */ package architecture import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/edit" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/shell" ) var ( _ = new(shell.Program) _ = prog.Run _ = eval.NewEvaler _ = (*eval.Evaler).Eval _ = parse.Parse _ = new(edit.Editor) _ = new(cli.App) _ = new(daemon.Program) _ = daemon.Activate ) elvish-0.21.0/docs/building.md000066400000000000000000000057731465720375400161640ustar00rootroot00000000000000# Building Elvish from source To build Elvish from source, you need - A supported OS: Linux, {Free,Net,Open}BSD, macOS, or Windows 10. Windows 10 support is experimental. - Go >= 1.21.0. To build Elvish from source, run one of the following commands: ```sh go install src.elv.sh/cmd/elvish@master # Install latest commit go install src.elv.sh/cmd/elvish@latest # Install latest released version go install src.elv.sh/cmd/elvish@v0.18.0 # Install a specific version ``` ## Controlling the installation location The [`go install`](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies) command installs Elvish to `$GOBIN`; the binary name is `elvish`. You can control the installation location by overriding `$GOBIN`, for example by prepending `env GOBIN=...` to the `go install` command. If `$GOBIN` is not set, the installation location defaults to `$GOPATH/bin`, which in turn defaults to `~/go/bin` if `$GOPATH` is also not set. The installation directory is probably not in your OS's default `$PATH`. You should either either add it to `$PATH`, or manually copy the Elvish binary to a directory already in `$PATH`. ## Building an alternative entrypoint In additional to `src.elv.sh/cmd/elvish` (which corresponds to the [`cmd/elvish`](./cmd/elvish) directory in the repo), there are a few alternative entrypoints, all named liked `cmd/*/elvish`, with slightly different feature sets. (From the perspective of Go, these are just different `main` packages.) For example, install the `cmd/withpprof/elvish` entrypoint to get [profiling support](https://pkg.go.dev/runtime/pprof) (change the part after `@` to get different versions): ```sh go install src.elv.sh/cmd/withpprof/elvish@master ``` ## Building from a local source tree If you are modifying Elvish's source code, you will want to clone Elvish's Git repository and build Elvish from the local source tree instead. To do this, run the following from the root of the source tree: ```sh go install ./cmd/elvish ``` There is no need to specify a version like `@master`; when inside a source tree, `go install` will always use the whatever source code is present. See [CONTRIBUTING.md](CONTRIBUTING.md) for more notes for contributors. ## Building with experimental plugin support Elvish has experimental support for building and importing plugins, modules written in Go. It relies on Go's [plugin support](https://pkg.go.dev/plugin), which is only available on a few platforms. Plugin support requires building Elvish with [cgo](https://pkg.go.dev/cmd/cgo). The official [prebuilt binaries](https://elv.sh/get) are built without cgo for compatibility and reproducibility, but by default the Go toolchain builds with cgo enabled. If you have built Elvish from source on a platform with plugin support, your Elvish build probably already supports plugins. To force cgo to be used when building Elvish, you can do the following: ```sh env CGO_ENABLED=1 go install ./cmd/elvish ``` To build a plugin, see this [example](https://github.com/elves/sample-plugin). elvish-0.21.0/docs/contributing.md000066400000000000000000000014341465720375400170640ustar00rootroot00000000000000# Process for contributing to Elvish The only person with direct commit access is the project's founder @xiaq. If you intend to make user-visible changes to Elvish's behavior (as opposed to fixing typos and obvious bugs), it is good idea to talk to him first; this will make it easier to review your changes. He should be reachable in the user group most of the time. 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. ## Licensing By contributing, you agree to license your code under the same license as existing source code of Elvish. See the [README](../README.md) at the project root for the license. elvish-0.21.0/docs/documenting.md000066400000000000000000000045731465720375400167000ustar00rootroot00000000000000# 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 written as "elvdocs", comment blocks before unindented `fn` or `var` declarations in Elvish files. A [large subset](https://pkg.go.dev/src.elv.sh/pkg/md@master) of [CommonMark](https://commonmark.org) is supported. Examples: ````elvish # Does something. # # Examples: # # ```elvish-transcript # ~> foo # some output # ``` fn foo {|a b c| } # Some variable. var bar ```` Most of Elvish's builtin modules are implemented in Go, not Elvish. For those modules, put dummy declarations in `.d.elv` files (`d` for "declaration"). For example, elvdocs for functions implemented in `builtin_fn_num.go` go in `builtin_fn_num.d.elv`. For a comment block to be considered an elvdoc, it has to be continuous, and each line should either be just `#` or start with `#` and a space. Style guides for elvdocs for functions: - The first sentence should start with a verb in 3rd person singular (i.e. ending with a "s"), as if there is an implicit subject "this function". - The end of the elvdoc should show 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`](https://elv.sh/ref/command.html#rc-file). It is quite common for elvdocs to link to other elvdocs, and Elvish's website toolchain provides special support for that. If a link has a single code span and an empty target, it gets rewritten to a link to an elvdoc section. For example, ``[`put`]()`` will get rewritten to ``[`put`](builtin.html#put)``, or just ``[`put`](#put)`` within the documentation for the builtin module. ## 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() { } ``` elvish-0.21.0/docs/elvish-as-library.md000066400000000000000000000020231465720375400177050ustar00rootroot00000000000000# Using Elvish as a library Elvish's implementation is structured as a collection of Go packages with well-documented internal APIs, so it's possible to use the parts you're interested in as a Go library. - Most likely, you'll want to use Elvish's interpreter. The examples for the [`Evaler.Eval` method](https://pkg.go.dev/src.elv.sh@master/pkg/eval#Evaler.Eval) should give you a good starting point. - For a general overview of how Elvish's code is structured, read the [architecture overview](https://pkg.go.dev/src.elv.sh@master/docs/architecture). However, beware that Elvish promises no backward compatibility in its Go API. The internal API surface is large, and will change from time to time as Elvish's implementation gets refactored. For now, this is consistent with Go's semantic versioning rules as Elvish is pre-1.0. When Elvish 1.0 is eventually released, all the internal libraries will likely be moved into an `internal` directory, with a small part of the API exposed via facades in the `pkg` directory. elvish-0.21.0/docs/packaging.md000066400000000000000000000014141465720375400162770ustar00rootroot00000000000000# Packaging Elvish The main package of Elvish is `cmd/elvish`, and you can build it like any other Go application. ## Enhancing version information You can set some variables in the `src.elv.sh/pkg/buildinfo` package using linker flags to enhance the Elvish's version information. See the [package's API doc](https://pkg.go.dev/src.elv.sh@master/pkg/buildinfo) for details. They don't affect any other aspect of Elvish's behavior, so it's infeasible to pass those linker flags, it's fine to leave them as is. **Note**: The names and usage of these variables have changed several time in Elvish's history. If your build script has `-ldflags '-X $symbol=$value'` where `$symbol` is not documented in the linked API doc, those flags no longer do anything and should be removed. elvish-0.21.0/docs/security.md000066400000000000000000000007131465720375400162230ustar00rootroot00000000000000# 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.21.0/docs/testing.md000066400000000000000000000062531465720375400160360ustar00rootroot00000000000000# Testing changes Write comprehensive unit tests for your code, and make sure that existing tests are passing. Run tests with `make test`. 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. ### Transcript tests Most tests against Elvish modules are written in `.elvts` files, which mimic transcripts of Elvish REPL sessions. See https://pkg.go.dev/src.elv.sh@master/pkg/transcript for the format of transcript files, and https://pkg.go.dev/src.elv.sh@master/pkg/eval/evaltest for details specific to using them as tests. If you use VS Code, the official Elvish extension allows you to simply press Alt-Enter to update the output of transcripts (specifically, the output for the code block the cursor is in). This means that you can author transcript tests entirely within the editor, instead of manually writing out the expected output or copy-pasting outputs from an actual REPL. **Note**: The functionality of the VS Code plugin is based on a very simple protocol and can be easily implemented for other editors. The protocol is documented in the godoc for the `evaltest` package (see link below), and you can also take a look `vscode/src/extension.ts` for the client implementation in the VS Code extension. ### 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`. ### Mocking dependencies Whenever possible, test the real thing. However, there are situations where it's infeasible to test the real thing, like syscall errors that can't be reliably triggered, or tests that rely on exact timing. In those cases, introduce a variable that stores the actual dependency (manual dependency injection): ```go // f.go package pkg import "os" var osSleep = os.Sleep func F() { // Use osSleep instead of os.Sleep } ``` And then use `testutil.Set` to override it for the duration of a test: ```go // f_test.go package pkg import "testing" func TestF(t *testing.T) { testutil.Set(&osSleep, func(d Duration) { // Fake implementation }) // Now test F } ``` If the test is in an external test package, the dependency variable will have to be exported. Instead of exporting it directly in the implementation file, export a pointer to it in a internal test file: ```go // testexport_test.go package pkg // Note: internal var OSSleep = &os.Sleep // f_test.go package pkg_test // Note: external import ( "pkg" "testing" ) func TestF(t *testing.T) { // Note: No more & since pkg.OSSleep is already a pointer testutil.Set(pkg.OSSleep, func(d Duration) { // Fake implementation }) // Now test F } ``` elvish-0.21.0/docs/workflows.md000066400000000000000000000072771465720375400164250ustar00rootroot00000000000000# Common development workflows The [`Makefile`](Makefile) encapsulates common development workflows: - Use `make fmt` to [format files](#formatting-files). - Use `make test` to [run tests](./testing.md). - Use `make all-checks` or `make most-checks` to [run checks](#running-checks). You can use the [`tools/pre-push`](../tools/pre-push) script as a Git hook, which runs all the tests and checks (`make test all-checks`), among other things. The same tests and checks are also run by Elvish's CI environments, so running them locally before pushing minimizes the chance of CI errors. (The CI environments run the tests on multiple platforms, so CI errors can still happen if you break some tests for a different platform.) ## Formatting files Use `make fmt` to format Go and Markdown files in the repo. ### Formatting Go files on save The Go plugins of most popular editors already support formatting Go files automatically on save; consult the documentation of the plugin you use. ### Formatting Markdown files on save The Markdown formatter is [`cmd/elvmdfmt`](../cmd/elvmdfmt), which lives inside this repo. Run it like this: ```sh go run src.elv.sh/cmd/elvmdfmt -width 80 -w $filename ``` To format Markdown files automatically on save, configure your editor to run the command above when saving Markdown files. You'll also want to configure this command to only run inside the Elvish repo, since `elvmdfmt` is tailored to Markdown files in this repo and may not work well for other Markdown files. If you use VS Code, install the [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) extension and add the following to the workspace (**not** user) `settings.json` file: ```json "emeraldwalk.runonsave": { "commands": [ { "match": "\\.md$", "cmd": "go run src.elv.sh/cmd/elvmdfmt -width 80 -w ${file}" } ] } ``` **Note**: Using `go run` ensures that you are always using the `elvmdfmt` implementation in the repo, but it incurs a small performance penalty since the Go toolchain does not cache binary files and has to rebuild it every time. If this is a problem (for example, if your editor runs the command synchronously), you can speed up the command by installing `src.elv.sh/cmd/elvmdfmt` and using the installed `elvmdfmt`. However, if you do this, you must re-install `elvmdfmt` whenever there is a change in its implementation that impacts the output. ## 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`. ## Running checks There are some checks on the source code that can be run with `make all-checks` or `make most-checks`. The difference is that `all-checks` includes a check ([`tools/check-gen.sh`](../tools/check-gen.sh)) that requires the Git repo to have a clean working tree, so may not be convenient to use when you are working on the source code. The `most-checks` target excludes that, so can be always be used. The checks depend on some external programs, which can be installed as follows: ```sh go install golang.org/x/tools/cmd/goimports@latest go install honnef.co/go/tools/cmd/staticcheck@v0.4.6 pip install --user codespell==2.2.6 ``` ## 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.21.0/go.mod000066400000000000000000000004441465720375400142110ustar00rootroot00000000000000module src.elv.sh require ( github.com/creack/pty v1.1.21 github.com/google/go-cmp v0.6.0 github.com/mattn/go-isatty v0.0.20 github.com/sourcegraph/jsonrpc2 v0.2.0 go.etcd.io/bbolt v1.3.9 golang.org/x/sync v0.6.0 golang.org/x/sys v0.17.0 pkg.nimblebun.works/go-lsp v1.1.0 ) go 1.21 elvish-0.21.0/go.sum000066400000000000000000000042761465720375400142450ustar00rootroot00000000000000github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pkg.nimblebun.works/go-lsp v1.1.0 h1:TH5ro4p2vlDtELK4LoVeKs4TsKm6aW1f5WP8jHm/9m4= pkg.nimblebun.works/go-lsp v1.1.0/go.mod h1:Suh759Ki+DjU0zwf0xkl1H6Ln1C6/+GtYyNofbtfcug= elvish-0.21.0/pkg/000077500000000000000000000000001465720375400136625ustar00rootroot00000000000000elvish-0.21.0/pkg/buildinfo/000077500000000000000000000000001465720375400156355ustar00rootroot00000000000000elvish-0.21.0/pkg/buildinfo/buildinfo.go000066400000000000000000000137131465720375400201440ustar00rootroot00000000000000// Package buildinfo contains build information. // // Exported string variables may be set during compilation using a linker flag // like this: // // go build -ldflags '-X src.elv.sh/pkg/buildinfo.NAME=VALUE' ./cmd/elvish // // This mechanism can be used by packagers to enhance Elvish's version // information. The variables that can be set are documented below. // // # BuildVariant // // [BuildVariant], if non-empty, gets appended to the version string along with // a "+" prefix. It should be set to a value identifying the build environment. // // Typically, this should be the name of the software distribution that is // packaging Elvish, possibly plus the revision of the package. Example for // revision 1 of a Debian package: // // go build -ldflags '-X src.elv.sh/pkg/buildinfo.BuildVariant=deb1' ./cmd/elvish // // Supposing that [VersionBase] is "0.233.0", this causes "elvish -version" to // print out "0.233.0+deb1". // // The value "official" is reserved for official binaries linked from // https://elv.sh/get. Do not use it unless you can ensure that your build is // bit-to-bit identical with the official binaries and you are committing to // maintaining that property. // // # VCSOverride // // On development commits, Elvish uses the information from Git to generate a // version string like (following the format of [Go module pseudo-versions]): // // 0.234.0-dev.0.20220320172241-5dc8c02a32cf // // where 20220320172241 is the commit time (in YYYYMMDDHHMMSS) and 5dc8c02a32cf // is the first 12 digits of the commit hash. // // If this information is not available when Elvish was built - for example, if // the build works from an archive of the commit rather than a Git checkout - // the version string will instead look like this: // // 0.234.0-dev.unknown // // In that case, [VCSOverride] can be set to to supply the $time-$commit // information: // // go build -ldflags '-X src.elv.sh/pkg/buildinfo.VCSOverride=20220320172241-5dc8c02a32cf' ./cmd/elvish // // Setting this variable is only necessary when building development commits and // the VCS information is not available. // // [Go module pseudo-versions]: https://go.dev/ref/mod#pseudo-versions package buildinfo import ( "encoding/json" "fmt" "os" "runtime" "runtime/debug" "strings" "time" "src.elv.sh/pkg/must" "src.elv.sh/pkg/prog" ) // VersionBase identifies the version of Elvish. // // - On release branches, it identifies the exact version of the commit, and // is consistent with the Git tag. For example, at tag v0.233.0, this will // be "0.233.0". // // - On development branches, it identifies the first version of the next // release branch. For example, after releases for 0.233.x has been branched // but before 0.234.x is branched, this will be "0.244.0". The full version // string will be augmented with VCS information (see [VCSOverride]). // // In both cases, the full version is also augmented with the [BuildVariant]. const VersionBase = "0.21.0" // VCSOverride may be set to identify the commit of development builds when that // information is not available during build time. It has no effect on release // branches. See the package godoc for more details. var VCSOverride string // BuildVariant may be set to identify the build environment. See the package // godoc for more details. var BuildVariant string // Type contains all the build information fields. type Type struct { Version string `json:"version"` GoVersion string `json:"goversion"` } func (Type) IsStructMap() {} // Value contains all the build information. var Value = Type{ // On a release branch, change to addVariant(VersionBase, BuildVariant). Version: addVariant(VersionBase, BuildVariant), GoVersion: runtime.Version(), } func addVariant(version, variant string) string { if variant != "" { version += "+" + variant } return version } var readBuildInfo = debug.ReadBuildInfo func devVersion(next, vcsOverride string) string { if vcsOverride != "" { return next + "-dev.0." + vcsOverride } fallback := next + "-dev.unknown" bi, ok := readBuildInfo() if !ok { return fallback } // If the main module's version is known, use it, but without the "v" // prefix. This is the case when Elvish is built with "go install // src.elv.sh/cmd/elvish@version". if v := bi.Main.Version; v != "" && v != "(devel)" { return strings.TrimPrefix(v, "v") } // If VCS information is available (i.e. when Elvish is built from a checked // out repo), build the version string with it. Emulate the format of pseudo // version (https://go.dev/ref/mod#pseudo-versions). m := make(map[string]string) for _, s := range bi.Settings { if k := strings.TrimPrefix(s.Key, "vcs."); k != s.Key { m[k] = s.Value } } if m["revision"] == "" || m["time"] == "" || m["modified"] == "" { return fallback } t, err := time.Parse(time.RFC3339Nano, m["time"]) if err != nil { return fallback } revision := m["revision"] if len(revision) > 12 { revision = revision[:12] } version := fmt.Sprintf("%s-dev.0.%s-%s", next, t.Format("20060102150405"), revision) if m["modified"] == "true" { return version + "-dirty" } return version } // Program is the buildinfo subprogram. type Program struct { version, buildinfo bool json *bool } func (p *Program) RegisterFlags(fs *prog.FlagSet) { fs.BoolVar(&p.version, "version", false, "Output the Elvish version and quit") fs.BoolVar(&p.buildinfo, "buildinfo", false, "Output information about the Elvish build and quit") p.json = fs.JSON() } func (p *Program) Run(fds [3]*os.File, _ []string) error { switch { case p.buildinfo: if *p.json { fmt.Fprintln(fds[1], mustToJSON(Value)) } else { fmt.Fprintln(fds[1], "Version:", Value.Version) fmt.Fprintln(fds[1], "Go version:", Value.GoVersion) } case p.version: if *p.json { fmt.Fprintln(fds[1], mustToJSON(Value.Version)) } else { fmt.Fprintln(fds[1], Value.Version) } default: return prog.NextProgram() } return nil } func mustToJSON(v any) string { return string(must.OK1(json.Marshal(v))) } elvish-0.21.0/pkg/buildinfo/buildinfo_test.elvts000066400000000000000000000014441465720375400217310ustar00rootroot00000000000000//each:elvish-in-global //////////////////// # program behavior # //////////////////// // Tests in this section are necessarily tautological, since we can't hardcode // the actual versions in the tests. Instead, all we do is verifying that the // output from the flags are consistent with the information in $buildinfo. ## -version ## ~> elvish -version | eq (one) $buildinfo[version] ▶ $true ~> elvish -version -json | eq (one) (to-json [$buildinfo[version]]) ▶ $true ## -buildinfo ## ~> elvish -buildinfo | eq (slurp) "Version: "$buildinfo[version]"\nGo version: "$buildinfo[go-version]"\n" ▶ $true ~> elvish -buildinfo -json | eq (one) (to-json [$buildinfo]) ▶ $true ## exits with NextProgram if neither flag is given ## ~> elvish [stderr] internal error: no suitable subprogram [exit] 2 elvish-0.21.0/pkg/buildinfo/buildinfo_test.go000066400000000000000000000050021465720375400211730ustar00rootroot00000000000000package buildinfo import ( "runtime/debug" "testing" "src.elv.sh/pkg/testutil" ) var devVersionTests = []struct { name string next string vcsOverride string buildInfo *debug.BuildInfo want string }{ { name: "no BuildInfo", next: "0.42.0", want: "0.42.0-dev.unknown", }, { name: "BuildInfo with Main.Version = (devel)", next: "0.42.0", buildInfo: &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}}, want: "0.42.0-dev.unknown", }, { name: "BuildInfo with non-empty Main.Version != (devel)", next: "0.42.0", buildInfo: &debug.BuildInfo{Main: debug.Module{Version: "v0.42.0-dev.foobar"}}, want: "0.42.0-dev.foobar", }, { name: "BuildInfo with VCS data from clean checkout", next: "0.42.0", buildInfo: &debug.BuildInfo{Settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "1234567890123456"}, {Key: "vcs.time", Value: "2022-04-01T23:59:58Z"}, {Key: "vcs.modified", Value: "false"}, }}, want: "0.42.0-dev.0.20220401235958-123456789012", }, { name: "BuildInfo with VCS data from dirty checkout", next: "0.42.0", buildInfo: &debug.BuildInfo{Settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "1234567890123456"}, {Key: "vcs.time", Value: "2022-04-01T23:59:58Z"}, {Key: "vcs.modified", Value: "true"}, }}, want: "0.42.0-dev.0.20220401235958-123456789012-dirty", }, { name: "BuildInfo with unknown VCS timestamp format", next: "0.42.0", buildInfo: &debug.BuildInfo{Settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "1234567890123456"}, {Key: "vcs.time", Value: "April First"}, {Key: "vcs.modified", Value: "false"}, }}, want: "0.42.0-dev.unknown", }, { name: "vcsOverride", next: "0.42.0", vcsOverride: "20220401235958-123456789012", want: "0.42.0-dev.0.20220401235958-123456789012", }, } func TestDevVersion(t *testing.T) { for _, test := range devVersionTests { t.Run(test.name, func(t *testing.T) { testutil.Set(t, &readBuildInfo, func() (*debug.BuildInfo, bool) { return test.buildInfo, test.buildInfo != nil }) got := devVersion(test.next, test.vcsOverride) if got != test.want { t.Errorf("got %q, want %q", got, test.want) } }) } } func TestAddVariant(t *testing.T) { got := addVariant("0.42.0", "") want := "0.42.0" if got != want { t.Errorf("got %q, want %q", got, want) } got = addVariant("0.42.0", "distro") want = "0.42.0+distro" if got != want { t.Errorf("got %q, want %q", got, want) } } elvish-0.21.0/pkg/buildinfo/transcripts_test.go000066400000000000000000000005371465720375400216040ustar00rootroot00000000000000package buildinfo_test import ( "embed" "testing" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/prog/progtest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvish-in-global", progtest.ElvishInGlobal(&buildinfo.Program{}), ) } elvish-0.21.0/pkg/cli/000077500000000000000000000000001465720375400144315ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/app.go000066400000000000000000000301111465720375400155340ustar00rootroot00000000000000// 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" "src.elv.sh/pkg/ui" ) // 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 ui.Text) } 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 []ui.Text // 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, QuotePaste: spec.QuotePaste, OnSubmit: a.CommitCode, State: spec.CodeAreaState, SimpleAbbreviations: spec.SimpleAbbreviations, CommandAbbreviations: spec.CommandAbbreviations, 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([]ui.Text(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 { handled = a.GlobalBindings.Handle(target, e) } if !handled { if k, ok := e.(term.KeyEvent); ok { a.Notify(ui.T("Unbound key: " + ui.Key(k).String())) } } 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 []ui.Text 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() a.codeArea.MutateState(func(s *tk.CodeAreaState) { s.HideTips = true s.HideRPrompt = hideRPrompt }) bufMain := renderApp([]tk.Widget{a.codeArea /* no addon */}, width, height) a.codeArea.MutateState(func(s *tk.CodeAreaState) { s.HideTips = false 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 []ui.Text, 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.WriteStyled(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 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. // TODO: Add a test for this. 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]*(n-rank) { heights[idx] = maxHeights[idx] } else { heights[idx] = remain / (n - rank) } remain -= heights[idx] } return heights, focus } func hasFocus(w any) 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 ui.Text) { a.MutateState(func(s *State) { s.Notes = append(s.Notes, note) }) a.Redraw() } elvish-0.21.0/pkg/cli/app_spec.go000066400000000000000000000040341465720375400165530ustar00rootroot00000000000000package 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 QuotePaste func() bool SimpleAbbreviations func(f func(abbr, full string)) CommandAbbreviations func(f func(abbr, full string)) 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 tips. Get(code string) (ui.Text, []ui.Text) // 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, []ui.Text) { 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.21.0/pkg/cli/app_test.go000066400000000000000000000362371465720375400166120ustar00rootroot00000000000000package 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, []ui.Text) { 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_ExceptInFinalRedraw(t *testing.T) { f := Setup(withHighlighter( testHighlighter{ get: func(code string) (ui.Text, []ui.Text) { tips := []ui.Text{ui.T("ERR 1"), ui.T("ERR 2")} return ui.T(code), tips }, })) 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) feedInput(f.TTY, "\n") f.TestTTY(t, "code", "\n", term.DotHere) } func TestReadCode_RedrawsOnLateUpdateFromHighlighter(t *testing.T) { var styling ui.Styling hl := testHighlighter{ get: func(code string) (ui.Text, []ui.Text) { 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 } // Event handling. 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_NotifiesAboutUnboundKey(t *testing.T) { f := Setup() defer f.Stop() f.TTY.Inject(term.K(ui.F1)) f.TestTTYNotes(t, "Unbound key: F1") } // Misc features. 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(ui.T("note")) f.App.Notify(ui.T("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, []ui.Text) lateUpdates chan struct{} } func (hl testHighlighter) Get(code string) (ui.Text, []ui.Text) { 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.21.0/pkg/cli/clitest/000077500000000000000000000000001465720375400161005ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/clitest/apptest.go000066400000000000000000000062201465720375400201070ustar00rootroot00000000000000// 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 ...any) *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 ...any) { 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 ...any) { 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.21.0/pkg/cli/clitest/apptest_test.go000066400000000000000000000016621465720375400211530ustar00rootroot00000000000000package clitest import ( "testing" "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/cli/tk" "src.elv.sh/pkg/ui" ) 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(ui.T("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.21.0/pkg/cli/clitest/fake_tty.go000066400000000000000000000167251465720375400202500ustar00rootroot00000000000000package 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 100ms, and aborts the // test if it doesn't. func (t TTYCtrl) TestBuffer(tt *testing.T, b *term.Buffer) { tt.Helper() ok := testBuffer(b, t.bufCh) if !ok { tt.Logf("wanted buffer not shown:\n%s", b.TTYString()) 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 } } } tt.FailNow() } } // TestNotesBuffer verifies that a notes buffer will appear within 100ms, and // aborts the test if it doesn't. func (t TTYCtrl) TestNotesBuffer(tt *testing.T, b *term.Buffer) { tt.Helper() ok := testBuffer(b, t.notesBufCh) if !ok { tt.Logf("wanted notes buffer not shown:\n%s", b.TTYString()) 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()) } } tt.FailNow() } } // 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 buffer appears on the channel within 100ms. func testBuffer(want *term.Buffer, ch <-chan *term.Buffer) bool { timeout := time.After(testutil.Scaled(100 * time.Millisecond)) for { select { case buf := <-ch: if reflect.DeepEqual(buf, want) { return true } case <-timeout: return false } } } elvish-0.21.0/pkg/cli/clitest/fake_tty_test.go000066400000000000000000000077761465720375400213150ustar00rootroot00000000000000package 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.21.0/pkg/cli/examples/000077500000000000000000000000001465720375400162475ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/e3bc/000077500000000000000000000000001465720375400170635ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/e3bc/bc/000077500000000000000000000000001465720375400174475ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/e3bc/bc/bc.go000066400000000000000000000017401465720375400203640ustar00rootroot00000000000000package 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.21.0/pkg/cli/examples/e3bc/completion.go000066400000000000000000000011371465720375400215650ustar00rootroot00000000000000package main import ( "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/ui" ) 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: ui.T(item), ToInsert: item} } return candidates } elvish-0.21.0/pkg/cli/examples/e3bc/main.go000066400000000000000000000033011465720375400203330ustar00rootroot00000000000000// 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/pkg/cli" "src.elv.sh/pkg/cli/examples/e3bc/bc" "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, []ui.Text) { 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.21.0/pkg/cli/examples/nav/000077500000000000000000000000001465720375400170335ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/nav/main.go000066400000000000000000000010031465720375400203000ustar00rootroot00000000000000// 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.21.0/pkg/cli/examples/widget/000077500000000000000000000000001465720375400175325ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/widget/main.go000066400000000000000000000030771465720375400210140ustar00rootroot00000000000000// 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.21.0/pkg/cli/examples/win_tty/000077500000000000000000000000001465720375400177445ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/examples/win_tty/main_windows.go000066400000000000000000000043621465720375400227760ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/000077500000000000000000000000001465720375400162765ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/histutil/db.go000066400000000000000000000005521465720375400172140ustar00rootroot00000000000000package histutil import ( "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) } elvish-0.21.0/pkg/cli/histutil/db_store.go000066400000000000000000000030471465720375400204320ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/db_store_test.go000066400000000000000000000017531465720375400214730ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/dedup_cursor.go000066400000000000000000000016771465720375400213360ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/dedup_cursor_test.go000066400000000000000000000012401465720375400223570ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/doc.go000066400000000000000000000001321465720375400173660ustar00rootroot00000000000000// Package histutil provides utilities for working with command history. package histutil elvish-0.21.0/pkg/cli/histutil/hybrid_store.go000066400000000000000000000032131465720375400213210ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/hybrid_store_test.go000066400000000000000000000123431465720375400223640ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/mem_store.go000066400000000000000000000025401465720375400206200ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/mem_store_test.go000066400000000000000000000005001465720375400216510ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/store.go000066400000000000000000000022761465720375400177700ustar00rootroot00000000000000package 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.21.0/pkg/cli/histutil/test_db.go000066400000000000000000000040241465720375400202510ustar00rootroot00000000000000package histutil import ( "strings" "src.elv.sh/pkg/store/storedefs" ) // 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() } 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() } 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.21.0/pkg/cli/loop.go000066400000000000000000000070601465720375400157340ustar00rootroot00000000000000package 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 any // 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.21.0/pkg/cli/loop_test.go000066400000000000000000000077431465720375400170030ustar00rootroot00000000000000package 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.21.0/pkg/cli/lscolors/000077500000000000000000000000001465720375400162715ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/lscolors/feature.go000066400000000000000000000050361465720375400202570ustar00rootroot00000000000000package lscolors import ( "os" "src.elv.sh/pkg/fsutil" ) type feature int const ( featureInvalid feature = iota featureOrphanedSymlink featureSymlink featureMultiHardLink featureNamedPipe featureSocket featureDoor featureBlockDevice featureCharDevice featureWorldWritableStickyDirectory featureWorldWritableDirectory featureStickyDirectory featureDirectory featureCapability featureSetuid featureSetgid featureExecutable featureRegular ) // Some platforms, such as Windows, have simulated Unix style permission masks. // On Windows the only two permission masks are 0o666 (RW) and 0o444 (RO). const worldWritable = 0o002 // Can be mutated in tests. var isDoorFunc = isDoor 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 isDoorFunc(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 fsutil.IsExecutable(stat): return featureExecutable, nil } // Check extension return featureRegular, nil } func is(m, p os.FileMode) bool { return m&p == p } elvish-0.21.0/pkg/cli/lscolors/feature_nonunix_test.go000066400000000000000000000003441465720375400230710ustar00rootroot00000000000000//go: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.21.0/pkg/cli/lscolors/feature_test.go000066400000000000000000000103751465720375400213200ustar00rootroot00000000000000package 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}) } testutil.Set(t, &isDoorFunc, func(info os.FileInfo) bool { return info.Name() == "door" }) err = create("door") test("door (fake)", "door", featureDoor, opt{setupErr: err}) 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}) // Regression test for b.elv.sh/1710. test("directory with mh=true", "d", featureDirectory, opt{setupErr: err, mh: true}) 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.21.0/pkg/cli/lscolors/feature_unix_test.go000066400000000000000000000002261465720375400223550ustar00rootroot00000000000000//go:build unix package lscolors import ( "golang.org/x/sys/unix" ) func createNamedPipe(fname string) error { return unix.Mkfifo(fname, 0600) } elvish-0.21.0/pkg/cli/lscolors/lscolors.go000066400000000000000000000110131465720375400204540ustar00rootroot00000000000000// 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.21.0/pkg/cli/lscolors/lscolors_test.go000066400000000000000000000022611465720375400215200ustar00rootroot00000000000000package lscolors import ( "os" "testing" "src.elv.sh/pkg/testutil" ) func TestLsColors(t *testing.T) { SetTestLsColors(t) testutil.InTempDir(t) os.Mkdir("dir", 0755) create("a.png") colorist := GetColorist() // Feature-based coloring. wantDirStyle := "34" if style := colorist.GetStyle("dir"); style != wantDirStyle { t.Errorf("Got dir style %q, want %q", style, wantDirStyle) } // Extension-based coloring. wantPngStyle := "31" if style := colorist.GetStyle("a.png"); style != wantPngStyle { t.Errorf("Got dir style %q, want %q", style, wantPngStyle) } } func TestLsColors_SkipsInvalidFields(t *testing.T) { testutil.Setenv(t, "LS_COLORS", "invalid=34:*.png=31") testutil.InTempDir(t) create("a.png") wantPngStyle := "31" if style := GetColorist().GetStyle("a.png"); style != wantPngStyle { t.Errorf("Got dir style %q, want %q", style, wantPngStyle) } } func TestLsColors_Default(t *testing.T) { testutil.Setenv(t, "LS_COLORS", "") testutil.InTempDir(t) create("a.png") // See defaultLsColorString wantPngStyle := "01;35" if style := GetColorist().GetStyle("a.png"); style != wantPngStyle { t.Errorf("Got dir style %q, want %q", style, wantPngStyle) } } elvish-0.21.0/pkg/cli/lscolors/stat_notsolaris.go000066400000000000000000000002221465720375400220440ustar00rootroot00000000000000//go:build !solaris package lscolors import "os" func isDoor(info os.FileInfo) bool { // Doors are only supported on Solaris. return false } elvish-0.21.0/pkg/cli/lscolors/stat_solaris.go000066400000000000000000000003161465720375400213270ustar00rootroot00000000000000package 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.21.0/pkg/cli/lscolors/stat_unix.go000066400000000000000000000010431465720375400206340ustar00rootroot00000000000000//go:build unix package lscolors import ( "os" "syscall" ) func isMultiHardlink(info os.FileInfo) bool { // The nlink field from stat considers all the "." and ".." references to // directories to be hard links, making all directories technically // multi-hardlink (one link from parent, one "." from itself, and one ".." // for every subdirectories). However, for the purpose of filename // highlighting, only regular files should ever be considered // multi-hardlink. return !info.IsDir() && info.Sys().(*syscall.Stat_t).Nlink > 1 } elvish-0.21.0/pkg/cli/lscolors/stat_windows.go000066400000000000000000000003431465720375400213450ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/000077500000000000000000000000001465720375400155405ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/modes/completion.go000066400000000000000000000050741465720375400202460ustar00rootroot00000000000000package modes import ( "errors" "strings" "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 ui.Text // 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(unstyle(candidate.ToShow)) { filtered = append(filtered, candidate) } } return filtered } func (it completionItems) Show(i int) ui.Text { return it[i].ToShow } func (it completionItems) Len() int { return len(it) } func unstyle(t ui.Text) string { var sb strings.Builder for _, seg := range t { sb.WriteString(seg.Text) } return sb.String() } elvish-0.21.0/pkg/cli/modes/completion_test.go000066400000000000000000000033511465720375400213010ustar00rootroot00000000000000package 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: ui.T("foo"), ToInsert: "foo"}, {ToShow: ui.T("foo bar", ui.FgBlue), ToInsert: "'foo bar'"}, }, }) 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.21.0/pkg/cli/modes/filter_spec.go000066400000000000000000000011411465720375400203630ustar00rootroot00000000000000package 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, []ui.Text) } 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.21.0/pkg/cli/modes/histlist.go000066400000000000000000000054141465720375400177360ustar00rootroot00000000000000package 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 // RPrompt of the code area (first row of the widget). CodeAreaRPrompt func() ui.Text } // 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) }, RPrompt: spec.CodeAreaRPrompt, 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.21.0/pkg/cli/modes/histlist_test.go000066400000000000000000000075471465720375400210060ustar00rootroot00000000000000package 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, []ui.Text) { 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.21.0/pkg/cli/modes/histwalk.go000066400000000000000000000057471465720375400177320ustar00rootroot00000000000000package 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 // Update buffer with current entry. Always returns a nil error. Accept() 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):], } }) } func (w *histwalk) Accept() error { w.attachedTo.MutateState((*tk.CodeAreaState).ApplyPending) w.app.PopAddon() return nil } elvish-0.21.0/pkg/cli/modes/histwalk_test.go000066400000000000000000000061071465720375400207600ustar00rootroot00000000000000package 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, "error: no history store", Styles, "!!!!!!") } 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, "error: end of history", Styles, "!!!!!!") // 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(ErrorText(err)) return } app.PushAddon(w) app.Redraw() } elvish-0.21.0/pkg/cli/modes/instant.go000066400000000000000000000044561465720375400175600ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/instant_test.go000066400000000000000000000031371465720375400206120ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/lastcmd.go000066400000000000000000000062031465720375400175170ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/lastcmd_test.go000066400000000000000000000052151465720375400205600ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/listing.go000066400000000000000000000044171465720375400175460ustar00rootroot00000000000000package 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 accepting. // 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.21.0/pkg/cli/modes/listing_test.go000066400000000000000000000043641465720375400206060ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/location.go000066400000000000000000000113321465720375400176770ustar00rootroot00000000000000package 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(ErrorText(err)) } 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.21.0/pkg/cli/modes/location_test.go000066400000000000000000000135531465720375400207450ustar00rootroot00000000000000package 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, "error: mock chdir error", Styles, "!!!!!!") // 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.21.0/pkg/cli/modes/mode.go000066400000000000000000000025231465720375400170150ustar00rootroot00000000000000// 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 // ErrorText returns a red "error:" followed by unstyled space and err.Error(). func ErrorText(err error) ui.Text { return ui.Concat(ui.T("error:", ui.FgRed), ui.T(" "), ui.T(err.Error())) } elvish-0.21.0/pkg/cli/modes/mode_test.go000066400000000000000000000024121465720375400200510ustar00rootroot00000000000000package 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" ) var Args = tt.Args func TestModeLine(t *testing.T) { testModeLine(t, modeLine) } func TestModePrompt(t *testing.T) { prompt := func(s string, b bool) ui.Text { return modePrompt(s, b)() } testModeLine(t, tt.Fn(prompt).Named("prompt")) } func testModeLine(t *testing.T, fn any) { tt.Test(t, fn, Args("TEST", false).Rets( ui.T("TEST", ui.Bold, ui.FgWhite, ui.BgMagenta)), 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(ErrorText(err)) } } 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.21.0/pkg/cli/modes/navigation.go000066400000000000000000000215451465720375400202350ustar00rootroot00000000000000package modes import ( "os" "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 // RPrompt of the code area (first row of the widget). CodeAreaRPrompt func() ui.Text } 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(ErrorText(err)) } else { w.codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer = tk.CodeBuffer{} }) w.lastFilter = "" 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(ErrorText(err)) } else { w.codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer = tk.CodeBuffer{} }) w.lastFilter = "" 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(os.Chdir) } 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) }, RPrompt: spec.CodeAreaRPrompt, 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.21.0/pkg/cli/modes/navigation_fs.go000066400000000000000000000116541465720375400207250ustar00rootroot00000000000000package 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(chdir func(string) error) NavigationCursor { return osCursor{chdir, lscolors.GetColorist()} } type osCursor struct { chdir func(string) error 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 c.chdir("..") } func (c osCursor) Descend(name string) error { return c.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 ( errNamedPipe = errors.New("no preview for named pipe") errDevice = errors.New("no preview for device file") 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.ModeNamedPipe, errNamedPipe}, {os.ModeDevice, errDevice}, {os.ModeSocket, errSocket}, {os.ModeCharDevice, errCharDevice}, } func (f file) Read() ([]NavigationFile, []byte, error) { // On Unix, opening a named pipe for reading is blocking when there are no // writers, so we need to do this check at the very beginning of this // function. // // TODO: There is still a chance that the file has changed between when // f.mode is populated and the os.Open call below, in which case the os.Open // call can still block. This can be fixed by opening the file in async mode // and setting a timeout on the reads. Reading the file asynchronously is // also desirable behavior more generally for the navigation mode to remain // usable on slower filesystems. if f.mode&os.ModeNamedPipe != 0 { return nil, nil, errNamedPipe } 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 } for _, special := range specialFileModes { if info.Mode()&special.mode != 0 { return nil, nil, special.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 } 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.21.0/pkg/cli/modes/navigation_fs_test.go000066400000000000000000000046271465720375400217660ustar00rootroot00000000000000package 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 any = 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 any } 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.21.0/pkg/cli/modes/navigation_test.go000066400000000000000000000227421465720375400212740ustar00rootroot00000000000000package 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/must" "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", "other.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, "error: cannot ascend", Styles, "!!!!!!") } 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, "error: cannot descend", Styles, "!!!!!!") } 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 TestNavigation_SelectedName(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_SelectedName_EmptyDirectory(t *testing.T) { f := Setup() defer f.Stop() cursor := &testCursor{ root: testutil.Dir{"d": testutil.Dir{}}, pwd: []string{"d"}} w := startNavigation(f.App, NavigationSpec{Cursor: cursor}) wantName := "" 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) must.Chdir("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 other.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 other.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, and // only shows d2 after filtering. w.MutateFiltering(func(bool) bool { return true }) f.TTY.Inject(term.K('2')) dFilter2Buf := f.MakeBuffer( "\n", " NAVIGATING 2", Styles, "************ ", term.DotHere, "\n", " a d2 d21 \n", Styles, " ############## ++++++++++++++++++++", " d d22 \n", Styles, "####", " f other.png ", Styles, " !!!!!!!!!!!!!!!!!!!!", ) f.TTY.TestBuffer(t, dFilter2Buf) // Unbound key while filtering is ignored. f.TTY.Inject(term.K('a', ui.Alt)) f.TTY.TestBuffer(t, dFilter2Buf) w.MutateFiltering(func(bool) bool { return false }) // Now move into d2, and test that the filter has been cleared when // descending. w.Descend() f.App.Redraw() f.TTY.TestBuffer(t, d21Buf) // Apply a filter within d2. w.MutateFiltering(func(bool) bool { return true }) f.TTY.Inject(term.K('2')) f.TestTTY(t, "\n", " NAVIGATING 2", Styles, "************ ", term.DotHere, "\n", " d1 d21 content d21\n", Styles, " ++++++++++++++", " d2 d22 \n", Styles, "####", " d3 ", Styles, "////", ) w.MutateFiltering(func(bool) bool { return false }) // Ascend, and test that the filter has been cleared again when ascending. w.Ascend() f.App.Redraw() f.TTY.TestBuffer(t, d2Buf) // Now move into d3, an empty directory. 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.21.0/pkg/cli/modes/navigation_unix_test.go000066400000000000000000000015531465720375400223340ustar00rootroot00000000000000//go:build unix package modes import ( "os" "syscall" "testing" . "src.elv.sh/pkg/cli/clitest" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) func TestNavigation_NoPreviewForNamedPipes(t *testing.T) { // A previous implementation tried to call os.Open on named pipes, which can // block indefinitely. Ensure that this no longer happens. testutil.InTempDir(t) must.OK(os.Mkdir("d", 0777)) must.OK(syscall.Mkfifo("d/pipe", 0666)) must.OK(os.Chdir("d")) f := setupNav(t) defer f.Stop() // Use the default real FS cursor. startNavigation(f.App, NavigationSpec{}) f.TestTTY(t, "", term.DotHere, "\n", " NAVIGATING \n", Styles, "************ ", " d pipe no preview for named\n", Styles, "#### ++++++++++++++ !!!!!!!!!!!!!!!!!!!!", " pipe", Styles, " !!!!!", ) } elvish-0.21.0/pkg/cli/modes/stub.go000066400000000000000000000021311465720375400170410ustar00rootroot00000000000000package 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.21.0/pkg/cli/modes/stub_test.go000066400000000000000000000014631465720375400201070ustar00rootroot00000000000000package 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.21.0/pkg/cli/prompt/000077500000000000000000000000001465720375400157525ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/prompt/prompt.go000066400000000000000000000066241465720375400176320ustar00rootroot00000000000000// 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.21.0/pkg/cli/prompt/prompt_test.go000066400000000000000000000104371465720375400206660ustar00rootroot00000000000000package 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 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.21.0/pkg/cli/term/000077500000000000000000000000001465720375400154005ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/term/buffer.go000066400000000000000000000110111465720375400171720ustar00rootroot00000000000000package 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.21.0/pkg/cli/term/buffer_builder.go000066400000000000000000000077411465720375400207170ustar00rootroot00000000000000package 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 ...any) *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.21.0/pkg/cli/term/buffer_builder_test.go000066400000000000000000000062671465720375400217600ustar00rootroot00000000000000package 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{" ", ""}}}}}, } // TestBufferBuilderWrites 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.21.0/pkg/cli/term/buffer_test.go000066400000000000000000000177151465720375400202520ustar00rootroot00000000000000package 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.21.0/pkg/cli/term/event.go000066400000000000000000000030201465720375400170430ustar00rootroot00000000000000package 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.21.0/pkg/cli/term/file_reader_unix.go000066400000000000000000000031111465720375400212270ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/cli/term/file_reader_unix_test.go000066400000000000000000000032041465720375400222710ustar00rootroot00000000000000//go:build unix package term import ( "fmt" "io" "os" "testing" "time" "src.elv.sh/pkg/must" "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 := must.Pipe() r, err := newFileReader(pr) if err != nil { panic(err) } return r, pw, func() { r.Close() pr.Close() pw.Close() } } elvish-0.21.0/pkg/cli/term/read_rune.go000066400000000000000000000020261465720375400176730ustar00rootroot00000000000000//go:build unix package 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.21.0/pkg/cli/term/read_rune_test.go000066400000000000000000000022571465720375400207400ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/cli/term/reader.go000066400000000000000000000027331465720375400171760ustar00rootroot00000000000000package 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.21.0/pkg/cli/term/reader_test.go000066400000000000000000000004771465720375400202400ustar00rootroot00000000000000package term import ( "errors" "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestIsReadErrorRecoverable(t *testing.T) { tt.Test(t, IsReadErrorRecoverable, Args(seqError{}).Rets(true), Args(ErrStopped).Rets(true), Args(errTimeout).Rets(true), Args(errors.New("other error")).Rets(false), ) } elvish-0.21.0/pkg/cli/term/reader_unix.go000066400000000000000000000274741465720375400202520ustar00rootroot00000000000000//go:build unix 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 being // 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.21.0/pkg/cli/term/reader_unix_test.go000066400000000000000000000135601465720375400213000ustar00rootroot00000000000000//go:build unix package term import ( "os" "strings" "testing" "src.elv.sh/pkg/must" "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 := must.Pipe() r := NewReader(pr) t.Cleanup(func() { r.Close() pr.Close() pw.Close() }) return r, pw } elvish-0.21.0/pkg/cli/term/reader_windows.go000066400000000000000000000141501465720375400207440ustar00rootroot00000000000000package term import ( "fmt" "io" "log" "os" "sync" "unicode/utf16" "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} var leadingSurrogate *surrogateKeyEvent 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 surrogate, ok := event.(surrogateKeyEvent); ok { if leadingSurrogate == nil { leadingSurrogate = &surrogate // Keep reading the trailing surrogate. continue } else { r := utf16.DecodeRune(leadingSurrogate.r, surrogate.r) return KeyEvent{Rune: r}, nil } } 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() //lint:ignore SA2001 We only lock the mutex to make sure that ReadEvent has //exited, so we unlock it immediately. 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 ) type surrogateKeyEvent struct{ r rune } func (surrogateKeyEvent) isEvent() {} // 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 { if utf16.IsSurrogate(r) { return surrogateKeyEvent{r} } else { 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.21.0/pkg/cli/term/reader_windows_test.go000066400000000000000000000032121465720375400220000ustar00rootroot00000000000000package term import ( "testing" "unicode/utf16" "src.elv.sh/pkg/sys/ewindows" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) func TestConvertEvent(t *testing.T) { r1, r2 := utf16.EncodeRune('😀') tt.Test(t, convertEvent, // 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(charKeyEvent(uint16(r1), 0)).Rets(surrogateKeyEvent{r1}), Args(charKeyEvent(uint16(r2), 0)).Rets(surrogateKeyEvent{r2}), 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.21.0/pkg/cli/term/setup.go000066400000000000000000000063211465720375400170710ustar00rootroot00000000000000package term import ( "fmt" "os" "src.elv.sh/pkg/sys" "src.elv.sh/pkg/wcwidth" ) // SetupForTUIOnce sets up the terminal once for a whole interactive session. It // returns a function that can be used to restore the original terminal config. func SetupForTUIOnce(in, out *os.File) func() { return setupForTUIOnce(in, out) } // SetupForTUI sets up the terminal so that it is suitable for the TUI // application (as implemented by pkg/cli). It returns a function that can be // used to restore the original terminal config. func SetupForTUI(in, out *os.File) (func() error, error) { return setupForTUI(in, out) } // SetupForEval sets up the terminal for evaluating Elvish code, whether or not // we are in an interactive session. It returns a function to call after the // evaluation finishes. func SetupForEval(in, out *os.File) func() { return setupForEval(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.21.0/pkg/cli/term/setup_unix.go000066400000000000000000000041661465720375400201410ustar00rootroot00000000000000//go:build unix package term import ( "fmt" "os" "golang.org/x/sys/unix" "src.elv.sh/pkg/errutil" "src.elv.sh/pkg/sys/eunix" ) func setupForTUIOnce(in, _ *os.File) func() { fd := int(in.Fd()) term, err := eunix.TermiosForFd(fd) if err != nil { return func() {} } savedTermios := term.Copy() // Turning off IXON frees up Ctrl-Q and Ctrl-S for keybindings, but it's not // actually necessary for Elvish to function. // // We do this in SetupForTUIOnce rather than SetupForTUI so that the user // can still use "stty ixon" to turn it on if they wish. // // Other "nice to have" terminal setups should go here as well. term.SetIXON(false) term.ApplyToFd(fd) return func() { savedTermios.ApplyToFd(fd) } } func setupForTUI(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 errutil.Multi(savedTermios.ApplyToFd(fd), restoreVT(out)) } return restore, errSetupVT } func setupForEval(in, out *os.File) func() { // There is nothing to set up on Unix, but we try to sanitize the terminal // when evaluation finishes. return func() { sanitize(in, out) } } 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.21.0/pkg/cli/term/setup_unix_test.go000066400000000000000000000014271465720375400211750ustar00rootroot00000000000000//go:build unix package term import ( "os" "testing" "github.com/creack/pty" ) func TestSetupForTUIOnce(t *testing.T) { _, tty := setupPTY(t) setupForTUIOnce(tty, tty) // TODO: Test whether the interesting flags in the termios were indeed set. } func TestSetupForTUI(t *testing.T) { _, tty := setupPTY(t) _, err := setupForTUI(tty, tty) if err != nil { t.Errorf("setupForTUI returns an error") } // TODO: Test whether the interesting flags in the termios were indeed set. // termios, err := eunix.TermiosForFd(int(tty.Fd())) } func setupPTY(t *testing.T) (ptySide, ttySide *os.File) { t.Helper() ptySide, ttySide, err := pty.Open() if err != nil { t.Skip("cannot open pty") } t.Cleanup(func() { ptySide.Close() ttySide.Close() }) return ptySide, ttySide } elvish-0.21.0/pkg/cli/term/setup_windows.go000066400000000000000000000030331465720375400206400ustar00rootroot00000000000000package term import ( "os" "golang.org/x/sys/windows" "src.elv.sh/pkg/errutil" ) const ( inMode = windows.ENABLE_WINDOW_INPUT | windows.ENABLE_MOUSE_INPUT | windows.ENABLE_PROCESSED_INPUT outMode = windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_WRAP_AT_EOL_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING ) func setupForTUIOnce(_, _ *os.File) func() { return func() {} } func setupForTUI(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, inMode) errSetOut := windows.SetConsoleMode(hOut, outMode) errVT := setupVT(out) return func() error { return errutil.Multi( restoreVT(out), windows.SetConsoleMode(hOut, oldOutMode), windows.SetConsoleMode(hIn, oldInMode)) }, errutil.Multi(errSetIn, errSetOut, errVT) } // We need ENABLE_VIRTUAL_TERMINAL_PROCESSING for styled text to function. This // includes texts created by the user (with the "styled" builtin) or by Elvish // itself (like exception stack traces). const outFlagForEval = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING func setupForEval(_, out *os.File) func() { h := windows.Handle(out.Fd()) var oldOutMode uint32 err := windows.GetConsoleMode(h, &oldOutMode) if err == nil { windows.SetConsoleMode(h, oldOutMode|outFlagForEval) } return func() {} } elvish-0.21.0/pkg/cli/term/setup_windows_test.go000066400000000000000000000032431465720375400217020ustar00rootroot00000000000000package term import ( "os" "testing" "golang.org/x/sys/windows" ) func TestSetupForEval(t *testing.T) { // open CONOUT$ manually because os.Stdout is redirected during testing out := openFile(t, "CONOUT$", os.O_RDWR, 0) defer out.Close() // Start with ENABLE_VIRTUAL_TERMINAL_PROCESSING initialOutMode := getConsoleMode(t, out) | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING setConsoleMode(t, out, initialOutMode) // Clear ENABLE_VIRTUAL_TERMINAL_PROCESSING modifiedOutMode := initialOutMode &^ windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING setConsoleMode(t, out, modifiedOutMode) // Check that SetupForEval sets ENABLE_VIRTUAL_TERMINAL_PROCESSING without // changing other bits restore := setupForEval(nil, out) if got := getConsoleMode(t, out); got != initialOutMode { t.Errorf("got console mode %v, want %v", got, initialOutMode) } // Check that restore is a no-op setConsoleMode(t, out, modifiedOutMode) restore() if got := getConsoleMode(t, out); got != modifiedOutMode { t.Errorf("got console mode %v, want %v", got, modifiedOutMode) } } func openFile(t *testing.T, name string, flag int, perm os.FileMode) *os.File { t.Helper() out, err := os.OpenFile(name, flag, perm) if err != nil { t.Fatalf("open %s: %v", name, err) } return out } func setConsoleMode(t *testing.T, file *os.File, mode uint32) { t.Helper() err := windows.SetConsoleMode(windows.Handle(file.Fd()), mode) if err != nil { t.Fatal("SetConsoleMode:", err) } } func getConsoleMode(t *testing.T, file *os.File) uint32 { t.Helper() var mode uint32 err := windows.GetConsoleMode(windows.Handle(file.Fd()), &mode) if err != nil { t.Fatal("GetConsoleMode:", err) } return mode } elvish-0.21.0/pkg/cli/term/term.go000066400000000000000000000002401465720375400166720ustar00rootroot00000000000000// Package term provides functionality for working with terminals. package term import "src.elv.sh/pkg/logutil" var logger = logutil.GetLogger("[cli/term] ") elvish-0.21.0/pkg/cli/term/writer.go000066400000000000000000000120231465720375400172410ustar00rootroot00000000000000package 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.21.0/pkg/cli/term/writer_test.go000066400000000000000000000007511465720375400203050ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/000077500000000000000000000000001465720375400150475ustar00rootroot00000000000000elvish-0.21.0/pkg/cli/tk/codearea.go000066400000000000000000000263741465720375400171550ustar00rootroot00000000000000package tk import ( "bytes" "regexp" "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 tips it has // found, such as errors and autofixes. If this function is not given, the // Widget does not highlight the code nor show any tips. Highlighter func(code string) (ui.Text, []ui.Text) // 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 no function is provided the Widget does not // expand any abbreviations of the specified type. SimpleAbbreviations func(f func(abbr, full string)) CommandAbbreviations 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 HideTips 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, []ui.Text) { 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.SimpleAbbreviations == nil { spec.SimpleAbbreviations = func(func(a, f string)) {} } if spec.CommandAbbreviations == nil { spec.CommandAbbreviations = 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 the state mutex is held. func (w *codeArea) expandSimpleAbbr() { var abbr, full string // Find the longest matching abbreviation. w.SimpleAbbreviations(func(a, f string) { if strings.HasSuffix(w.inserts, a) && len(a) > len(abbr) { abbr, full = a, f } }) if len(abbr) > 0 { buf := &w.State.Buffer *buf = CodeBuffer{ Content: buf.Content[:buf.Dot-len(abbr)] + full + buf.Content[buf.Dot:], Dot: buf.Dot - len(abbr) + len(full), } w.resetInserts() } } var commandRegex = regexp.MustCompile(`(?:^|[^^]\n|\||;|{\s|\()\s*([\p{L}\p{M}\p{N}!%+,\-./:@\\_<>*]+)(\s)$`) // Tries to expand a command abbreviation. This function assumes the state mutex // is held. // // We use a regex rather than parse.Parse() because dealing with the latter // requires a lot of code. A simple regex is far simpler and good enough for // this use case. The regex essentially matches commands at the start of the // line (with potential leading whitespace) and similarly after the opening // brace of a lambda or pipeline char. // // This only handles bareword commands. func (w *codeArea) expandCommandAbbr() { buf := &w.State.Buffer if buf.Dot < len(buf.Content) { // Command abbreviations are only expanded when inserting at the end of the buffer. return } // See if there is something that looks like a bareword at the end of the buffer. matches := commandRegex.FindStringSubmatch(buf.Content) if len(matches) == 0 { return } // Find an abbreviation matching the command. command, whitespace := matches[1], matches[2] var expansion string w.CommandAbbreviations(func(a, e string) { if a == command { expansion = e } }) if expansion == "" { return } // We found a matching abbreviation -- replace it with its expansion. newContent := buf.Content[:buf.Dot-len(command)-1] + expansion + whitespace *buf = CodeBuffer{ Content: newContent, Dot: len(newContent), } w.resetInserts() } // Try to expand a small word abbreviation. This function assumes the state mutex is held. func (w *codeArea) expandSmallWordAbbr(trigger rune, categorizer func(rune) int) { buf := &w.State.Buffer if buf.Dot < len(buf.Content) { // Word abbreviations are only expanded when inserting 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(buf.Content) > len(a)+triggerLen { r1, _ := utf8.DecodeLastRuneInString(buf.Content[:len(buf.Content)-len(a)-triggerLen]) r2, _ := utf8.DecodeRuneInString(a) if categorizer(r1) == categorizer(r2) { return } } abbr, full = a, f }) if len(abbr) > 0 { *buf = CodeBuffer{ Content: buf.Content[:buf.Dot-len(abbr)-triggerLen] + full + string(trigger), Dot: buf.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 if parse.IsWhitespace(key.Rune) { w.expandCommandAbbr() } w.expandSimpleAbbr() w.expandSmallWordAbbr(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.21.0/pkg/cli/tk/codearea_render.go000066400000000000000000000054611465720375400205060ustar00rootroot00000000000000package 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 tips []ui.Text } 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 s.HideTips { errors = nil } 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) } } for _, tip := range v.tips { buf.Newline() buf.WriteStyled(tip) } } 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.21.0/pkg/cli/tk/codearea_test.go000066400000000000000000000412331465720375400202030ustar00rootroot00000000000000package tk import ( "reflect" "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var Args = tt.Args 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, []ui.Text) { 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: "tips", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("> ")), Highlighter: func(code string) (ui.Text, []ui.Text) { return ui.T(code), []ui.Text{ui.T("static error")} }, State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}), Width: 10, Height: 24, Want: bb(10).Write("> code").SetDotHere(). Newline().Write("static error"), }, { Name: "hiding tips", Given: NewCodeArea(CodeAreaSpec{ Prompt: p(ui.T("> ")), Highlighter: func(code string) (ui.Text, []ui.Text) { return ui.T(code), []ui.Text{ui.T("static error")} }, State: CodeAreaState{ Buffer: CodeBuffer{Content: "code", Dot: 4}, HideTips: true}}), Width: 10, Height: 24, Want: bb(10).Write("> code").SetDotHere(), }, { 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{ SimpleAbbreviations: 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{ SimpleAbbreviations: 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{ SimpleAbbreviations: 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{ SimpleAbbreviations: 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{ SimpleAbbreviations: 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: "command abbreviation expansion", Given: NewCodeArea(CodeAreaSpec{ CommandAbbreviations: 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: "command abbreviation expansion not at start of line", Given: NewCodeArea(CodeAreaSpec{ CommandAbbreviations: func(f func(abbr, full string)) { f("eh", "echo hello") }, }), Events: []term.Event{term.K('x'), term.K('|'), term.K('e'), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x|echo hello ", Dot: 13}}, }, { Name: "command abbreviation expansion at start of second line", Given: NewCodeArea(CodeAreaSpec{ CommandAbbreviations: func(f func(abbr, full string)) { f("eh", "echo hello") }, State: CodeAreaState{Buffer: CodeBuffer{Content: "echo\n", Dot: 5}}, }), Events: []term.Event{term.K('e'), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo\necho hello ", Dot: 16}}, }, { Name: "no command abbreviation expansion when not in command position", Given: NewCodeArea(CodeAreaSpec{ CommandAbbreviations: func(f func(abbr, full string)) { f("eh", "echo hello") }, }), Events: []term.Event{term.K('x'), term.K(' '), term.K('e'), term.K('h'), term.K(' ')}, WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x eh ", Dot: 5}}, }, { 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{ SimpleAbbreviations: 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, applyPending, Args(CodeAreaState{Buffer: CodeBuffer{}, Pending: PendingCode{0, 0, "ls"}}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "ls", Dot: 2}, Pending: PendingCode{}}), 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. Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}}), // HideRPrompt is kept intact. Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, HideRPrompt: true}). Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}, HideRPrompt: true}), ) } elvish-0.21.0/pkg/cli/tk/colview.go000066400000000000000000000123051465720375400170470ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/colview_test.go000066400000000000000000000065051465720375400201130ustar00rootroot00000000000000package 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, distribute, // Nice integer distributions. Args(10, []int{1, 1}).Rets([]int{5, 5}), Args(10, []int{2, 3}).Rets([]int{4, 6}), Args(10, []int{1, 2, 2}).Rets([]int{2, 4, 4}), // Approximate integer distributions. Args(10, []int{1, 1, 1}).Rets([]int{3, 3, 4}), Args(5, []int{1, 1, 1}).Rets([]int{1, 2, 2}), ) } elvish-0.21.0/pkg/cli/tk/combobox.go000066400000000000000000000043371465720375400172150ustar00rootroot00000000000000package 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 { // TODO: Test the behavior of Render when height is very small // (https://b.elv.sh/1820) if height == 1 { return w.listBox.Render(width, height) } buf := w.codeArea.Render(width, height-1) 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.21.0/pkg/cli/tk/combobox_test.go000066400000000000000000000053511465720375400202510ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/empty.go000066400000000000000000000007721465720375400165420ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/label.go000066400000000000000000000014711465720375400164600ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/layout_test.go000066400000000000000000000047111465720375400177550ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/listbox.go000066400000000000000000000254331465720375400170710ustar00rootroot00000000000000package 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 layout. 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 [NewListBox], 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, scrollbar := getHorizontalWindow(s, w.Padding, width, height) if scrollbar { return h + 1 } 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 var colHeight int w.mutate(func(s *ListBoxState) { if s.Items == nil || s.Items.Len() == 0 { s.First = 0 } else { s.First, s.ContentHeight, _ = getHorizontalWindow(*s, w.Padding, width, height) colHeight = s.ContentHeight } 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 += colHeight { selectedRow := -1 // Render the column starting from i. col := make([]ui.Text, 0, colHeight) for j := i; j < i+colHeight && 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+colHeight) if colWidth > remainedWidth { colWidth = remainedWidth hasCropped = true } colBuf := croppedLines{ lines: col, padding: w.Padding, selectFrom: selectedRow, selectTo: selectedRow + 1, extendStyle: w.ExtendStyle}.Render(colWidth, colHeight) 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 colHeight < height && (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.ContentHeight = 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 && len(left) > 0 { 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 // [ListBox.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 // [ListBox.Select]. // // TODO(xiaq): This does not correctly with multi-line items. func PrevPage(s ListBoxState) int { return fixIndex(s.Selected-s.ContentHeight, 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 // [ListBox.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 // [ListBox.Select]. // // TODO(xiaq): This does not correctly with multi-line items. func NextPage(s ListBoxState) int { return fixIndex(s.Selected+s.ContentHeight, 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 // [ListBox.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 // [ListBox.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 [ListBox.Select]. func Left(s ListBoxState) int { return horizontal(s.Selected, s.Items.Len(), -s.ContentHeight) } // Right moves the selection to the item to the right. It is only meaningful in // horizontal layout and suitable as an argument to [ListBox.Select]. func Right(s ListBoxState) int { return horizontal(s.Selected, s.Items.Len(), s.ContentHeight) } 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.21.0/pkg/cli/tk/listbox_state.go000066400000000000000000000023311465720375400202610ustar00rootroot00000000000000package tk import ( "fmt" "src.elv.sh/pkg/ui" ) // ListBoxState keeps the mutable state ListBox. type ListBoxState struct { Items Items Selected int // The first element to show. Used when rendering and adjusted accordingly // when the terminal size changes or the user has scrolled. First int // Height of the listbox, excluding horizontal scrollbar when using the // horizontal layout. Stored in the state for commands to move the cursor by // page (for vertical layout) or column (for horizontal layout). ContentHeight 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.21.0/pkg/cli/tk/listbox_test.go000066400000000000000000000331071465720375400201250ustar00rootroot00000000000000package 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.ContentHeight; 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), }, { Name: "not showing scrollbar with height = 1", Given: NewListBox(ListBoxSpec{ Horizontal: true, State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), Width: 10, Height: 1, Want: bb(10). Write("item 0", ui.Inverse).Write(" it"), }, } 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.ContentHeight; 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}, ContentHeight: 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.21.0/pkg/cli/tk/listbox_window.go000066400000000000000000000120141465720375400204470ustar00rootroot00000000000000package 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, // the height of each column, and whether a scrollbar may be shown. func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int, bool) { 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, false } // At this point, assume that we'll have to use the entire available height // and show a scrollbar, unless height is 1, in which case we'd rather use the // one line to show some actual content and give up the scrollbar. // // This is rather pessimistic, but until an efficient // algorithm that generates a more optimal layout emerges we'll use this // simple one. scrollbar := false if height > 1 { scrollbar = true 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, scrollbar } 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.21.0/pkg/cli/tk/listbox_window_test.go000066400000000000000000000105441465720375400215140ustar00rootroot00000000000000package tk import ( "testing" "src.elv.sh/pkg/tt" ) func TestGetVerticalWindow(t *testing.T) { tt.Test(t, getVerticalWindow, // 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, getHorizontalWindow, // 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.21.0/pkg/cli/tk/scrollbar.go000066400000000000000000000040531465720375400173630ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/textview.go000066400000000000000000000062101465720375400172540ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/textview_test.go000066400000000000000000000070431465720375400203200ustar00rootroot00000000000000package 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.21.0/pkg/cli/tk/utils_test.go000066400000000000000000000064751465720375400176110ustar00rootroot00000000000000package 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 any 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 any) any { return reflectState(v).Interface() } func setState(v, state any) { reflectState(v).Set(reflect.ValueOf(state)) } func reflectState(v any) 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.21.0/pkg/cli/tk/widget.go000066400000000000000000000036331465720375400166660ustar00rootroot00000000000000// 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.21.0/pkg/cli/tk/widget_test.go000066400000000000000000000040471465720375400177250ustar00rootroot00000000000000package 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.21.0/pkg/cli/tty.go000066400000000000000000000053071465720375400156050ustar00rootroot00000000000000package 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.SetupForTUI(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.21.0/pkg/cli/tty_unix_test.go000066400000000000000000000016271465720375400177100ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/daemon/000077500000000000000000000000001465720375400151255ustar00rootroot00000000000000elvish-0.21.0/pkg/daemon/activate.go000066400000000000000000000140621465720375400172570ustar00rootroot00000000000000package 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.21.0/pkg/daemon/activate_test.go000066400000000000000000000057721465720375400203260ustar00rootroot00000000000000package daemon import ( "io" "net" "os" "runtime" "testing" "time" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/must" "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. must.CreateEmpty("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) must.CreateEmpty("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) testutil.Set(t, &startProcess, f) scaleDuration(t, &daemonSpawnTimeout) scaleDuration(t, &daemonKillTimeout) } func scaleDuration(t *testing.T, d *time.Duration) { testutil.Set(t, 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.21.0/pkg/daemon/activate_unix_test.go000066400000000000000000000027431465720375400213640ustar00rootroot00000000000000//go:build unix 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/must" ) 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 }) must.MkdirAll("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.21.0/pkg/daemon/client.go000066400000000000000000000105441465720375400167360ustar00rootroot00000000000000package 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 any) 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 } elvish-0.21.0/pkg/daemon/daemondefs/000077500000000000000000000000001465720375400172325ustar00rootroot00000000000000elvish-0.21.0/pkg/daemon/daemondefs/daemondefs.go000066400000000000000000000017351465720375400216740ustar00rootroot00000000000000// 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.21.0/pkg/daemon/internal/000077500000000000000000000000001465720375400167415ustar00rootroot00000000000000elvish-0.21.0/pkg/daemon/internal/api/000077500000000000000000000000001465720375400175125ustar00rootroot00000000000000elvish-0.21.0/pkg/daemon/internal/api/api.go000066400000000000000000000030671465720375400206200ustar00rootroot00000000000000// 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 } elvish-0.21.0/pkg/daemon/server.go000066400000000000000000000105041465720375400167620ustar00rootroot00000000000000// 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" "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/rpc" "src.elv.sh/pkg/store" ) var logger = logutil.GetLogger("[daemon] ") // Program is the daemon subprogram. type Program struct { run bool paths *prog.DaemonPaths // Used in tests. serveOpts ServeOpts } func (p *Program) RegisterFlags(fs *prog.FlagSet) { fs.BoolVar(&p.run, "daemon", false, "[internal flag] Run the storage daemon instead of an Elvish shell") p.paths = fs.DaemonPaths() } func (p *Program) Run(fds [3]*os.File, args []string) error { if !p.run { return prog.NextProgram() } if len(args) > 0 { return prog.BadUsage("arguments are not allowed with -daemon") } // The stdout is redirected to a unique log file (see the spawn function), // so just use it for logging. logutil.SetOutput(fds[1]) setUmaskForDaemon() exit := Serve(p.paths.Sock, p.paths.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 // ServeOpts 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 { // Ignore the error - if we can't close the connection it's because // the client has closed it. There is nothing we can do anyway. conn.Close() } } 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.21.0/pkg/daemon/server_test.elvts000066400000000000000000000010711465720375400205500ustar00rootroot00000000000000//each:elvish-in-global //////////////////// # error conditions # //////////////////// ## no -daemon flag ## ~> elvish [stderr] internal error: no suitable subprogram [exit] 2 ## superfluous arguments ## ~> elvish -daemon x &check-stderr-contains='arguments are not allowed with -daemon' [stderr contains "arguments are not allowed with -daemon"] true [exit] 2 ## can't listen to socket ## //in-temp-dir ~> print > sock ~> elvish -daemon -sock sock -db db &check-stdout-contains='failed to listen on sock' [stdout contains "failed to listen on sock"] true [exit] 2 elvish-0.21.0/pkg/daemon/server_test.go000066400000000000000000000070031465720375400200210ustar00rootroot00000000000000package daemon import ( "os" "syscall" "testing" "time" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/daemon/internal/api" "src.elv.sh/pkg/must" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/store/storetest" "src.elv.sh/pkg/testutil" ) 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) } func TestProgram_StillServesIfCannotOpenDB(t *testing.T) { setup(t) must.WriteFile("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 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 struct{}) devNull := must.OK1(os.OpenFile(os.DevNull, os.O_RDWR, 0)) go func() { prog.Run( [3]*os.File{devNull, devNull, devNull}, args, &Program{serveOpts: opts}) 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() devNull.Close() }) return s } type server struct { t *testing.T ch <-chan struct{} } func (s server) WaitQuit() bool { s.t.Helper() select { case <-s.ch: return true case <-time.After(testutil.Scaled(2 * time.Second)): s.t.Error("timed out waiting for daemon to quit") return 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.21.0/pkg/daemon/server_unix_test.go000066400000000000000000000011731465720375400210660ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/daemon/service.go000066400000000000000000000047041465720375400171210ustar00rootroot00000000000000package 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 } elvish-0.21.0/pkg/daemon/sys_unix.go000066400000000000000000000007321465720375400173370ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/daemon/sys_windows.go000066400000000000000000000015561465720375400200530ustar00rootroot00000000000000package 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.21.0/pkg/daemon/transcripts_test.go000066400000000000000000000005261465720375400210720ustar00rootroot00000000000000package daemon_test import ( "embed" "testing" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/prog/progtest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvish-in-global", progtest.ElvishInGlobal(&daemon.Program{}), ) } elvish-0.21.0/pkg/diag/000077500000000000000000000000001465720375400145665ustar00rootroot00000000000000elvish-0.21.0/pkg/diag/context.go000066400000000000000000000074421465720375400166100ustar00rootroot00000000000000package diag import ( "fmt" "strings" ) // Context stores information derived from a range in some text. It is used for // errors that point to a part of the source code, including parse errors, // compilation errors and a single traceback entry in an exception. // // Context values should only be constructed using [NewContext]. type Context struct { Name string Ranging // 1-based line and column numbers of the start position. StartLine, StartCol int // 1-based line and column numbers of the end position, inclusive. Note that // if the range is zero-width, EndCol will be StartCol - 1. EndLine, EndCol int // The relevant text, text before its the first line and the text after its // last line. Body, Head, Tail string } // NewContext creates a new Context. func NewContext(name, source string, r Ranger) *Context { rg := r.Range() d := getContextDetails(source, rg) return &Context{name, rg, d.startLine, d.startCol, d.endLine, d.endCol, d.body, d.head, d.tail} } // Show shows the context. // // If the body has only one line, it returns one line like: // // foo.elv:12:7-11: lorem ipsum // // If the body has multiple lines, it shows the body in an indented block: // // foo.elv:12:1-13:5 // lorem // ipsum // // The body is underlined. func (c *Context) Show(indent string) string { rangeDesc := c.describeRange() if c.StartLine == c.EndLine { // Body has only one line, show it on the same line: // return fmt.Sprintf("%s: %s", rangeDesc, showContextText(indent, c.Head, c.Body, c.Tail)) } indent += " " return fmt.Sprintf("%s:\n%s%s", rangeDesc, indent, showContextText(indent, c.Head, c.Body, c.Tail)) } func (c *Context) describeRange() string { if c.StartLine == c.EndLine { if c.EndCol < c.StartCol { // Since EndCol is inclusive, zero-width ranges result in EndCol = // StartCol - 1. return fmt.Sprintf("%s:%d:%d", c.Name, c.StartLine, c.StartCol) } return fmt.Sprintf("%s:%d:%d-%d", c.Name, c.StartLine, c.StartCol, c.EndCol) } return fmt.Sprintf("%s:%d:%d-%d:%d", c.Name, c.StartLine, c.StartCol, c.EndLine, c.EndCol) } // Variables controlling the style used in [*Context.Show]. Can be overridden in // tests. var ( ContextBodyStartMarker = "\033[1;4m" ContextBodyEndMarker = "\033[m" ) func showContextText(indent, head, body, tail string) string { var sb strings.Builder sb.WriteString(head) for i, line := range strings.Split(body, "\n") { if i > 0 { sb.WriteByte('\n') sb.WriteString(indent) } sb.WriteString(ContextBodyStartMarker) sb.WriteString(line) sb.WriteString(ContextBodyEndMarker) } sb.WriteString(tail) return sb.String() } // Information about the lines that contain the culprit. type contextDetails struct { startLine, startCol int endLine, endCol int body, head, tail string } func getContextDetails(source string, r Ranging) contextDetails { before := source[:r.From] body := source[r.From:r.To] after := source[r.To:] head := lastLine(before) // If the body ends with a newline, stripe it, and leave the tail empty. // Otherwise, don't process the body and calculate the tail. var tail string if strings.HasSuffix(body, "\n") { body = body[:len(body)-1] } else { tail = firstLine(after) } startLine := strings.Count(before, "\n") + 1 startCol := 1 + len(head) endLine := startLine + strings.Count(body, "\n") var endCol int if startLine == endLine { endCol = startCol + len(body) - 1 } else { endCol = len(lastLine(body)) } return contextDetails{startLine, startCol, endLine, endCol, body, head, tail} } 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.21.0/pkg/diag/context_test.go000066400000000000000000000025641465720375400176470ustar00rootroot00000000000000package diag import ( "strings" "testing" ) var sourceRangeTests = []struct { Name string Context *Context Indent string WantShow string }{ { Name: "single-line culprit", Context: contextInParen("[test]", "echo (bad)"), Indent: "_", WantShow: dedent(` [test]:1:6-10: echo <(bad)>`), }, { Name: "multi-line culprit", Context: contextInParen("[test]", "echo (bad\nbad)\nmore"), Indent: "_", WantShow: dedent(` [test]:1:6-2:4: _ echo <(bad> _ `), }, { Name: "trailing newline in culprit is removed", Context: NewContext("[test]", "echo bad\n", Ranging{5, 9}), Indent: "_", WantShow: dedent(` [test]:1:6-8: echo `), }, { Name: "empty culprit", Context: NewContext("[test]", "echo x", Ranging{5, 5}), WantShow: dedent(` [test]:1:6: echo <>x`), }, } func TestContext(t *testing.T) { setContextBodyMarkers(t, "<", ">") 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) } }) } } // Returns a Context with the given name and source, and a range for the part // between ( and ). func contextInParen(name, src string) *Context { return NewContext(name, src, Ranging{strings.Index(src, "("), strings.Index(src, ")") + 1}) } elvish-0.21.0/pkg/diag/doc.go000066400000000000000000000001571465720375400156650ustar00rootroot00000000000000// Package diag contains building blocks for formatting and processing // diagnostic information. package diag elvish-0.21.0/pkg/diag/error.go000066400000000000000000000064511465720375400162540ustar00rootroot00000000000000package diag import ( "fmt" "strings" "src.elv.sh/pkg/strutil" ) // Error represents an error with context that can be showed. type Error[T ErrorTag] struct { Message string Context Context // Indicates whether the error may be caused by partial input. More // formally, this field should be true iff there exists a string x such that // appending it to the input eliminates the error. Partial bool } // ErrorTag is used to parameterize [Error] into different concrete types. The // ErrorTag method is called with a zero receiver, and its return value is used // in [Error.Error] and [Error.Show]. type ErrorTag interface { ErrorTag() string } // RangeError combines error with [Ranger]. type RangeError interface { error Ranger } // Error returns a plain text representation of the error. func (e *Error[T]) Error() string { return errorTag[T]() + ": " + e.errorNoType() } func (e *Error[T]) errorNoType() string { return e.Context.describeRange() + ": " + e.Message } // Range returns the range of the error. func (e *Error[T]) Range() Ranging { return e.Context.Range() } var ( messageStart = "\033[31;1m" messageEnd = "\033[m" ) // Show shows the error. func (e *Error[T]) Show(indent string) string { return errorTagTitle[T]() + ": " + e.showNoType(indent) } func (e *Error[T]) showNoType(indent string) string { indent += " " return messageStart + e.Message + messageEnd + "\n" + indent + e.Context.Show(indent) } // PackErrors packs multiple instances of [Error] with the same tag into one // error: // // - If called with no errors, it returns nil. // // - If called with one error, it returns that error itself. // // - If called with more than one [Error], it returns an error that combines // all of them. The returned error also implements [Shower], and its Error // and Show methods only print the tag once. func PackErrors[T ErrorTag](errs []*Error[T]) error { switch len(errs) { case 0: return nil case 1: return errs[0] default: return append(multiError[T](nil), errs...) } } // UnpackErrors returns the constituent [Error] instances in an error if it is // built from [PackErrors]. Otherwise it returns nil. func UnpackErrors[T ErrorTag](err error) []*Error[T] { switch err := err.(type) { case *Error[T]: return []*Error[T]{err} case multiError[T]: return append([]*Error[T](nil), err...) default: return nil } } type multiError[T ErrorTag] []*Error[T] func (err multiError[T]) Error() string { var sb strings.Builder fmt.Fprintf(&sb, "multiple %s: ", errorTagPlural[T]()) for i, e := range err { if i > 0 { sb.WriteString("; ") } sb.WriteString(e.errorNoType()) } return sb.String() } func (err multiError[T]) Show(indent string) string { var sb strings.Builder fmt.Fprintf(&sb, "Multiple %s:", errorTagPlural[T]()) indent += " " for _, e := range err { sb.WriteString("\n" + indent) sb.WriteString(e.showNoType(indent)) } return sb.String() } func errorTag[T ErrorTag]() string { var t T return t.ErrorTag() } // We don't have any error tags with an irregular plural yet. When we do, we can // let ErrorTag optionally implement interface{ ErrorTagPlural() } and use that // when available. func errorTagPlural[T ErrorTag]() string { return errorTag[T]() + "s" } func errorTagTitle[T ErrorTag]() string { return strutil.Title(errorTag[T]()) } elvish-0.21.0/pkg/diag/error_test.go000066400000000000000000000055311465720375400173110ustar00rootroot00000000000000package diag import ( "errors" "reflect" "testing" "github.com/google/go-cmp/cmp" ) type fooErrorTag struct{} func (fooErrorTag) ErrorTag() string { return "foo error" } func TestError(t *testing.T) { setContextBodyMarkers(t, "<", ">") setMessageMarkers(t, "{", "}") err := &Error[fooErrorTag]{ Message: "bad list", Context: *contextInParen("[test]", "echo (x)"), } wantErrorString := "foo error: [test]:1:6-8: 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) } // Title() is used for Show wantShow := dedent(` Foo error: {bad list} [test]:1:6-8: echo <(x)>`) if gotShow := err.Show(""); gotShow != wantShow { t.Errorf("Show() -> %q, want %q", gotShow, wantShow) } } var ( err1 = &Error[fooErrorTag]{ Message: "bad 1", Context: *contextInParen("a.elv", "echo (1)"), } err2 = &Error[fooErrorTag]{ Message: "bad 2", Context: *contextInParen("b.elv", "echo (2\n0)"), } ) var multiErrorsTests = []struct { name string errs []*Error[fooErrorTag] wantErr error }{ { name: "no error", errs: nil, wantErr: nil, }, { name: "one error", errs: []*Error[fooErrorTag]{err1}, wantErr: err1, }, { name: "multiple errors", errs: []*Error[fooErrorTag]{err1, err2}, wantErr: multiError[fooErrorTag]{err1, err2}, }, } func TestPackAndUnpackErrors(t *testing.T) { for _, tc := range multiErrorsTests { t.Run(tc.name, func(t *testing.T) { err := PackErrors(tc.errs) if !reflect.DeepEqual(err, tc.wantErr) { t.Errorf("got packed error %#v, want %#v", err, tc.wantErr) } unpacked := UnpackErrors[fooErrorTag](err) if !reflect.DeepEqual(unpacked, tc.errs) { t.Errorf("Unpacked: (-want +got):\n%s", cmp.Diff(tc.errs, unpacked)) } }) } } func TestUnpackErrors_CalledWithOtherErrorType(t *testing.T) { unpacked := UnpackErrors[fooErrorTag](errors.New("foo")) if unpacked != nil { t.Errorf("want nil, got %v", unpacked) } } func TestMultiError_ErrorAndShow(t *testing.T) { setContextBodyMarkers(t, "<", ">") setMessageMarkers(t, "{", "}") err := PackErrors([]*Error[fooErrorTag]{err1, err2}) wantError := "multiple foo errors: a.elv:1:6-8: bad 1; b.elv:1:6-2:2: bad 2" if s := err.Error(); s != wantError { t.Errorf(".Error() returns unexpected result (-want +got):\n%s", cmp.Diff(wantError, s)) } wantShow := dedent(` Multiple foo errors: {bad 1} a.elv:1:6-8: echo <(1)> {bad 2} b.elv:1:6-2:2: echo <(2> <0)>`) if show := err.(Shower).Show(""); show != wantShow { t.Errorf(".Show(\"\") returns unexpected result (-want +got):\n%s", cmp.Diff(wantShow, show)) } } elvish-0.21.0/pkg/diag/range.go000066400000000000000000000016271465720375400162170ustar00rootroot00000000000000package 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. // // Ideally, this type would be called Range. However, doing that means structs // embedding this type will have Range as a field instead of a method, thus not // implementing 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.21.0/pkg/diag/range_test.go000066400000000000000000000011231465720375400172450ustar00rootroot00000000000000package 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, PointRanging, Args(1).Rets(Ranging{1, 1}), ) } func TestMixedRanging(t *testing.T) { tt.Test(t, MixedRanging, Args(Ranging{1, 2}, Ranging{0, 4}).Rets(Ranging{1, 4}), Args(Ranging{0, 4}, Ranging{1, 2}).Rets(Ranging{0, 2}), ) } elvish-0.21.0/pkg/diag/show_error.go000066400000000000000000000006011465720375400173030ustar00rootroot00000000000000package diag import ( "fmt" "io" ) // ShowError shows an error. It uses the Show method if the error // implements Shower. Otherwise, it prints the error in bold and red, with a // trailing newline. func ShowError(w io.Writer, err error) { if shower, ok := err.(Shower); ok { fmt.Fprintln(w, shower.Show("")) } else { fmt.Fprintf(w, "\033[31;1m%s\033[m\n", err.Error()) } } elvish-0.21.0/pkg/diag/show_error_test.go000066400000000000000000000012541465720375400203470ustar00rootroot00000000000000package 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.21.0/pkg/diag/shower.go000066400000000000000000000002271465720375400164250ustar00rootroot00000000000000package diag // Shower wraps the Show function. type Shower interface { // Show takes an indentation string and shows. Show(indent string) string } elvish-0.21.0/pkg/diag/testutil_test.go000066400000000000000000000006041465720375400200310ustar00rootroot00000000000000package diag import ( "testing" "src.elv.sh/pkg/testutil" ) var dedent = testutil.Dedent func setContextBodyMarkers(t *testing.T, start, end string) { testutil.Set(t, &ContextBodyStartMarker, start) testutil.Set(t, &ContextBodyEndMarker, end) } func setMessageMarkers(t *testing.T, start, end string) { testutil.Set(t, &messageStart, start) testutil.Set(t, &messageEnd, end) } elvish-0.21.0/pkg/diff/000077500000000000000000000000001465720375400145725ustar00rootroot00000000000000elvish-0.21.0/pkg/diff/LICENSE000066400000000000000000000027071465720375400156050ustar00rootroot00000000000000Copyright (c) 2009 The Go Authors. 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. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 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 OWNER 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.21.0/pkg/diff/diff.go000066400000000000000000000171761465720375400160450ustar00rootroot00000000000000// Copyright 2022 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 diff import ( "bytes" "fmt" "sort" "strings" ) // A pair is a pair of values tracked for both the x and y side of a diff. // It is typically a pair of line indexes. type pair struct{ x, y int } // Diff returns an anchored diff of the two texts old and new // in the “unified diff” format. If old and new are identical, // Diff returns a nil slice (no output). // // Unix diff implementations typically look for a diff with // the smallest number of lines inserted and removed, // which can in the worst case take time quadratic in the // number of lines in the texts. As a result, many implementations // either can be made to run for a long time or cut off the search // after a predetermined amount of work. // // In contrast, this implementation looks for a diff with the // smallest number of “unique” lines inserted and removed, // where unique means a line that appears just once in both old and new. // We call this an “anchored diff” because the unique lines anchor // the chosen matching regions. An anchored diff is usually clearer // than a standard diff, because the algorithm does not try to // reuse unrelated blank lines or closing braces. // The algorithm also guarantees to run in O(n log n) time // instead of the standard O(n²) time. // // Some systems call this approach a “patience diff,” named for // the “patience sorting” algorithm, itself named for a solitaire card game. // We avoid that name for two reasons. First, the name has been used // for a few different variants of the algorithm, so it is imprecise. // Second, the name is frequently interpreted as meaning that you have // to wait longer (to be patient) for the diff, meaning that it is a slower algorithm, // when in fact the algorithm is faster than the standard one. func Diff(oldName, old, newName, new string) []byte { if old == new { return nil } // Print diff header. var out bytes.Buffer fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) fmt.Fprintf(&out, "--- %s\n", oldName) fmt.Fprintf(&out, "+++ %s\n", newName) out.Write(DiffNoHeader(old, new)) return out.Bytes() } func DiffNoHeader(old, new string) []byte { x := lines(old) y := lines(new) var out bytes.Buffer // Loop over matches to consider, // expanding each match to include surrounding lines, // and then printing diff chunks. // To avoid setup/teardown cases outside the loop, // tgs returns a leading {0,0} and trailing {len(x), len(y)} pair // in the sequence of matches. var ( done pair // printed up to x[:done.x] and y[:done.y] chunk pair // start lines of current chunk count pair // number of lines from each side in current chunk ctext []string // lines for current chunk ) for _, m := range tgs(x, y) { if m.x < done.x { // Already handled scanning forward from earlier match. continue } // Expand matching lines as far possible, // establishing that x[start.x:end.x] == y[start.y:end.y]. // Note that on the first (or last) iteration we may (or definitey do) // have an empty match: start.x==end.x and start.y==end.y. start := m for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { start.x-- start.y-- } end := m for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { end.x++ end.y++ } // Emit the mismatched lines before start into this chunk. // (No effect on first sentinel iteration, when start = {0,0}.) for _, s := range x[done.x:start.x] { ctext = append(ctext, "-"+s) count.x++ } for _, s := range y[done.y:start.y] { ctext = append(ctext, "+"+s) count.y++ } // If we're not at EOF and have too few common lines, // the chunk includes all the common lines and continues. const C = 3 // number of context lines if (end.x < len(x) || end.y < len(y)) && (end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { for _, s := range x[start.x:end.x] { ctext = append(ctext, " "+s) count.x++ count.y++ } done = end continue } // End chunk with common lines for context. if len(ctext) > 0 { n := end.x - start.x if n > C { n = C } for _, s := range x[start.x : start.x+n] { ctext = append(ctext, " "+s) count.x++ count.y++ } done = pair{start.x + n, start.y + n} // Format and emit chunk. // Convert line numbers to 1-indexed. // Special case: empty file shows up as 0,0 not 1,0. if count.x > 0 { chunk.x++ } if count.y > 0 { chunk.y++ } fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) for _, s := range ctext { out.WriteString(s) } count.x = 0 count.y = 0 ctext = ctext[:0] } // If we reached EOF, we're done. if end.x >= len(x) && end.y >= len(y) { break } // Otherwise start a new chunk. chunk = pair{end.x - C, end.y - C} for _, s := range x[chunk.x:end.x] { ctext = append(ctext, " "+s) count.x++ count.y++ } done = end } return out.Bytes() } // lines returns the lines in the file x, including newlines. // If the file does not end in a newline, one is supplied // along with a warning about the missing newline. func lines(x string) []string { l := strings.SplitAfter(x, "\n") if l[len(l)-1] == "" { l = l[:len(l)-1] } else { // Treat last line as having a message about the missing newline attached, // using the same text as BSD/GNU diff (including the leading backslash). l[len(l)-1] += "\n\\ No newline at end of file\n" } return l } // tgs returns the pairs of indexes of the longest common subsequence // of unique lines in x and y, where a unique line is one that appears // once in x and once in y. // // The longest common subsequence algorithm is as described in // Thomas G. Szymanski, “A Special Case of the Maximal Common // Subsequence Problem,” Princeton TR #170 (January 1975), // available at https://research.swtch.com/tgs170.pdf. func tgs(x, y []string) []pair { // Count the number of times each string appears in a and b. // We only care about 0, 1, many, counted as 0, -1, -2 // for the x side and 0, -4, -8 for the y side. // Using negative numbers now lets us distinguish positive line numbers later. m := make(map[string]int) for _, s := range x { if c := m[s]; c > -2 { m[s] = c - 1 } } for _, s := range y { if c := m[s]; c > -8 { m[s] = c - 4 } } // Now unique strings can be identified by m[s] = -1+-4. // // Gather the indexes of those strings in x and y, building: // xi[i] = increasing indexes of unique strings in x. // yi[i] = increasing indexes of unique strings in y. // inv[i] = index j such that x[xi[i]] = y[yi[j]]. var xi, yi, inv []int for i, s := range y { if m[s] == -1+-4 { m[s] = len(yi) yi = append(yi, i) } } for i, s := range x { if j, ok := m[s]; ok && j >= 0 { xi = append(xi, i) inv = append(inv, j) } } // Apply Algorithm A from Szymanski's paper. // In those terms, A = J = inv and B = [0, n). // We add sentinel pairs {0,0}, and {len(x),len(y)} // to the returned sequence, to help the processing loop. J := inv n := len(xi) T := make([]int, n) L := make([]int, n) for i := range T { T[i] = n + 1 } for i := 0; i < n; i++ { k := sort.Search(n, func(k int) bool { return T[k] >= J[i] }) T[k] = J[i] L[i] = k + 1 } k := 0 for _, v := range L { if k < v { k = v } } seq := make([]pair, 2+k) seq[1+k] = pair{len(x), len(y)} // sentinel at end lastj := n for i := n - 1; i >= 0; i-- { if L[i] == k && J[i] < lastj { seq[k] = pair{xi[i], yi[J[i]]} k-- } } seq[0] = pair{0, 0} // sentinel at start return seq } elvish-0.21.0/pkg/edit/000077500000000000000000000000001465720375400146075ustar00rootroot00000000000000elvish-0.21.0/pkg/edit/binding_map.go000066400000000000000000000076501465720375400174150ustar00rootroot00000000000000package edit import ( "errors" "sort" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "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 { vals.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 any) (any, error) { key, err := toKey(index) if err != nil { return nil, err } return vals.Index(bt.Map, key) } func (bt bindingsMap) HasKey(k any) 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 any) (any, 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 any) any { 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 vals.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 } type bindingTipEntry struct { text string fnNames []string } func bindingTip(text string, fnNames ...string) bindingTipEntry { return bindingTipEntry{text, fnNames} } // Given a binding map and a list of function groups, returns a text describing // the keys that are bound to any function in each group. // // This uses Elvish qnames for both the binding map and the functions because // the place that calls bindingTips may not have direct access to them. func bindingTips(ns *eval.Ns, binding string, entries ...bindingTipEntry) ui.Text { m := getVar(ns, binding).(bindingsMap) var t ui.Text for _, entry := range entries { values := make([]any, len(entry.fnNames)) for i, fnName := range entry.fnNames { values[i] = getVar(ns, fnName+eval.FnSuffix) } keys := keysBoundTo(m, values) if len(keys) == 0 { continue } if len(t) > 0 { t = ui.Concat(t, ui.T(" ")) } for _, k := range keys { t = ui.Concat(t, ui.T(k.String(), ui.Inverse), ui.T(" ")) } t = ui.Concat(t, ui.T(entry.text)) } return t } func getVar(ns *eval.Ns, qname string) any { segs := eval.SplitQNameSegs(qname) for _, seg := range segs[:len(segs)-1] { ns = ns.IndexString(seg).Get().(*eval.Ns) } return ns.IndexString(segs[len(segs)-1]).Get() } func keysBoundTo(m bindingsMap, values []any) []ui.Key { var keys []ui.Key for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() for _, value := range values { if v == value { keys = append(keys, k.(ui.Key)) continue } } } return keys } elvish-0.21.0/pkg/edit/binding_map_test.elvts000066400000000000000000000024711465720375400212000ustar00rootroot00000000000000//each:binding-map-in-global /////////////// # binding-map # /////////////// ## checking key and value when constructing ## ~> binding-map [&[]={ }] Exception: must be key or string [tty]:1:1-21: binding-map [&[]={ }] ~> binding-map [&foo={ }] Exception: bad key: foo [tty]:1:1-22: binding-map [&foo={ }] ~> binding-map [&a=string] Exception: value should be function [tty]:1:1-23: binding-map [&a=string] ## repr ## // prints like an ordinary map ~> repr (binding-map [&]) [&] // keys are always sorted ~> repr (binding-map [&a=$nop~ &b=$nop~ &c=$nop~]) [&a= &b= &c=] ## indexing ## ~> eq $nop~ (binding-map [&a=$nop~])[a] ▶ $true // checking key ~> put (binding-map [&a=$nop~])[foo] Exception: bad key: foo [tty]:1:5-33: put (binding-map [&a=$nop~])[foo] ## assoc ## ~> count (assoc (binding-map [&a=$nop~]) b $nop~) ▶ (num 2) // checking key ~> (assoc (binding-map [&a=$nop~]) foo $nop~) Exception: bad key: foo [tty]:1:2-41: (assoc (binding-map [&a=$nop~]) foo $nop~) // checking value ~> (assoc (binding-map [&a=$nop~]) b foo) Exception: value should be function [tty]:1:2-37: (assoc (binding-map [&a=$nop~]) b foo) ## dissoc ## ~> count (dissoc (binding-map [&a=$nop~]) a) ▶ (num 0) // allows bad key - no op ~> count (dissoc (binding-map [&a=$nop~]) foo) ▶ (num 1) elvish-0.21.0/pkg/edit/binding_map_test.go000066400000000000000000000010241465720375400204410ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) // The happy path of bindingHelp is tested in modes that use bindingHelp. func TestBindingHelp_NoBinding(t *testing.T) { ns := eval.BuildNs(). AddGoFn("a", func() {}). AddVar("binding", vars.FromInit(bindingsMap{vals.EmptyMap})). Ns() // A bindings map with no relevant binding if got := bindingTips(ns, "binding", bindingTip("do a", "a")); len(got) > 0 { t.Errorf("got %v, want empty text", got) } } elvish-0.21.0/pkg/edit/buffer_builtins.d.elv000066400000000000000000000060571465720375400207330ustar00rootroot00000000000000# Moves the dot left one rune. Does nothing if the dot is at the beginning of # the buffer. fn move-dot-left { } # Kills one rune left of the dot. Does nothing if the dot is at the beginning of # the buffer. fn kill-rune-left { } # Moves the dot right one rune. Does nothing if the dot is at the end of the # buffer. fn move-dot-right { } # Kills one rune right of the dot. Does nothing if the dot is at the end of the # buffer. fn kill-rune-left { } # Moves the dot to the start of the current line. fn move-dot-sol { } # Deletes the text between the dot and the start of the current line. fn kill-line-left { } # Moves the dot to the end of the current line. fn move-dot-eol { } # Deletes the text between the dot and the end of the current line. fn kill-line-right { } # 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. fn move-dot-up { } # 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. fn move-dot-down { } # 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. fn transpose-rune { } # Moves the dot to the beginning of the last word to the left of the dot. fn move-dot-left-word { } # Deletes the last word to the left of the dot. fn kill-word-left { } # Moves the dot to the beginning of the first word to the right of the dot. fn move-dot-right-word { } # Deletes the first word to the right of the dot. fn kill-word-right { } # 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. fn transpose-word { } # Moves the dot to the beginning of the last small word to the left of the dot. fn move-dot-left-small-word { } # Deletes the last small word to the left of the dot. fn kill-small-word-left { } # Moves the dot to the beginning of the first small word to the right of the dot. fn move-dot-right-small-word { } # Deletes the first small word to the right of the dot. fn kill-small-word-right { } # 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. fn transpose-small-word { } # Moves the dot to the beginning of the last alnum word to the left of the dot. fn move-dot-left-alnum-word { } # Deletes the last alnum word to the left of the dot. fn kill-alnum-word-left { } # Moves the dot to the beginning of the first alnum word to the right of the dot. fn move-dot-right-alnum-word { } # Deletes the first alnum word to the right of the dot. fn kill-alnum-word-right { } # 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. fn transpose-alnum-word { } elvish-0.21.0/pkg/edit/buffer_builtins.go000066400000000000000000000317521465720375400203300ustar00rootroot00000000000000package 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]any) 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-alnum-word-left": makeKill(moveDotLeftAlnumWord), "kill-alnum-word-right": 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. func moveDotLeft(buffer string, dot int) int { _, w := utf8.DecodeLastRuneInString(buffer[:dot]) return dot - w } func moveDotRight(buffer string, dot int) int { _, w := utf8.DecodeRuneInString(buffer[dot:]) return dot + w } func moveDotSOL(buffer string, dot int) int { return strutil.FindLastSOL(buffer[:dot]) } func moveDotEOL(buffer string, dot int) int { return strutil.FindFirstEOL(buffer[dot:]) + dot } 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)) } 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)) } 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 } func moveDotLeftWord(buffer string, dot int) int { return moveDotLeftGeneralWord(categorizeWord, buffer, dot) } func moveDotRightWord(buffer string, dot int) int { return moveDotRightGeneralWord(categorizeWord, buffer, dot) } 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 } } func moveDotLeftSmallWord(buffer string, dot int) int { return moveDotLeftGeneralWord(tk.CategorizeSmallWord, buffer, dot) } func moveDotRightSmallWord(buffer string, dot int) int { return moveDotRightGeneralWord(tk.CategorizeSmallWord, buffer, dot) } func transposeSmallWord(buffer string, dot int) (string, int) { return transposeGeneralWord(tk.CategorizeSmallWord, buffer, dot) } func moveDotLeftAlnumWord(buffer string, dot int) int { return moveDotLeftGeneralWord(categorizeAlnum, buffer, dot) } func moveDotRightAlnumWord(buffer string, dot int) int { return moveDotRightGeneralWord(categorizeAlnum, buffer, dot) } 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 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 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.21.0/pkg/edit/builtins.d.elv000066400000000000000000000032531465720375400173750ustar00rootroot00000000000000# Converts a normal map into a binding map. fn binding-table {|map| } # Closes the current active mode. fn close-mode { } # Adds a notification saying "End of history". fn end-of-history { } # 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. fn redraw {|&full=$false| } # Clears the screen. # # This command should be used in place of the external `clear` command to clear # the screen. fn clear { } # Requests the next terminal input to be inserted uninterpreted. fn insert-raw { } # Parses a string into a key. fn key {|string| } # Prints a notification message. The argument may be a string or a [styled # text](builtin.html#styled). # # 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. fn notify {|message| } # 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. fn return-line { } # Causes the Elvish REPL to terminate. If called from a key binding, takes # effect after the key binding returns. fn return-eof { } # If the current code is syntactically incomplete (like `echo [`), inserts a # literal newline. # # Otherwise, applies any pending autofixes and accepts the current line. fn smart-enter { } # Breaks Elvish code into words. fn wordify {|code| } elvish-0.21.0/pkg/edit/builtins.go000066400000000000000000000076151465720375400170000ustar00rootroot00000000000000package 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/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/parseutil" "src.elv.sh/pkg/ui" ) func closeMode(app cli.App) { app.PopAddon() } func endOfHistory(app cli.App) { app.Notify(ui.T("End of history")) } type redrawOpts struct{ Full bool } func (redrawOpts) SetDefaultOptions() {} func redraw(app cli.App, opts redrawOpts) { if opts.Full { app.RedrawFull() } else { app.Redraw() } } func clear(app cli.App, tty cli.TTY) { tty.HideCursor() tty.ClearScreen() app.RedrawFull() tty.ShowCursor() } 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) } var errMustBeKeyOrString = errors.New("must be key or string") func toKey(v any) (ui.Key, error) { switch v := v.(type) { case ui.Key: return v, nil case string: return ui.ParseKey(v) default: return ui.Key{}, errMustBeKeyOrString } } func notify(app cli.App, x any) error { // TODO: De-duplicate with the implementation of the styled builtin. var t ui.Text switch x := x.(type) { case string: t = ui.T(x) case ui.Text: t = x.Clone() default: return errs.BadValue{What: "argument to edit:notify", Valid: "string, styled segment or styled text", Actual: vals.Kind(x)} } app.Notify(t) return nil } func smartEnter(ed *Editor) { codeArea, ok := focusedCodeArea(ed.app) if !ok { return } insertedNewline := false codeArea.MutateState(func(s *tk.CodeAreaState) { buf := &s.Buffer if !isSyntaxComplete(buf.Content) { buf.InsertAtDot("\n") insertedNewline = true } }) if insertedNewline { return } // TODO: Check whether the code area is actually the main code area. This // isn't a problem for now because smart-enter is only bound to Enter in // $edit:insert:binding, which is used by the main code area. // // TODO: This is prone to race condition if the code area was just mutated. ed.applyAutofix() ed.app.CommitCode() } func isSyntaxComplete(code string) bool { _, err := parse.Parse(parse.Source{Name: "[syntax check]", Code: code}, parse.Config{}) for _, e := range parse.UnpackErrors(err) { if e.Context.From == len(code) { return false } } return true } 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]any{ "insert-raw": func() { insertRaw(app, tty) }, "clear": func() { clear(app, tty) }, }) } func initMiscBuiltins(ed *Editor, nb eval.NsBuilder) { nb.AddGoFns(map[string]any{ "binding-table": makeBindingMap, "close-mode": func() { closeMode(ed.app) }, "end-of-history": func() { endOfHistory(ed.app) }, "key": toKey, "notify": func(x any) error { return notify(ed.app, x) }, "redraw": func(opts redrawOpts) { redraw(ed.app, opts) }, "return-line": ed.app.CommitCode, "return-eof": ed.app.CommitEOF, "smart-enter": func() { smartEnter(ed) }, "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(modes.ErrorText(err)) return nil, false } return codeArea, true } elvish-0.21.0/pkg/edit/builtins_test.elvts000066400000000000000000000003631465720375400205600ustar00rootroot00000000000000//each:wordify-in-global /////////// # wordify # /////////// ~> wordify 'ls str [list]' ▶ ls ▶ str ▶ '[list]' // propagates output errors ~> wordify foo >&- Exception: port does not support value output [tty]:1:1-15: wordify foo >&- elvish-0.21.0/pkg/edit/builtins_test.go000066400000000000000000000345271465720375400200410ustar00rootroot00000000000000package 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/tt" "src.elv.sh/pkg/ui" ) func TestBindingTable(t *testing.T) { f := setup(t) evals(f.Evaler, `var called = $false`) evals(f.Evaler, `var m = (edit:binding-table [&a={ set 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 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, `var 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, `set 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, `set 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 TestNotify(t *testing.T) { f := setup(t) evals(f.Evaler, "edit:notify string") f.TestTTYNotes(t, "string") evals(f.Evaler, "edit:notify (styled styled red)") f.TestTTYNotes(t, "styled", Styles, "!!!!!!") evals(f.Evaler, "var err = ?(edit:notify [])") if _, hasErr := getGlobal(f.Evaler, "err").(error); !hasErr { t.Errorf("calling edit:notify with [] did not result in error") // TODO: Test the exact error } } 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) } } // TODO: Test that smart-enter applies autofix. 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, "error: "+modes.ErrFocusedWidgetNotCodeArea.Error(), Styles, "!!!!!!") }) } } // Tests for pure movers. func TestMoveDotLeftRight(t *testing.T) { tt.Test(t, moveDotLeft, Args("foo", 0).Rets(0), Args("bar", 3).Rets(2), Args("精灵", 0).Rets(0), Args("精灵", 3).Rets(0), Args("精灵", 6).Rets(3), ) tt.Test(t, moveDotRight, Args("foo", 0).Rets(1), Args("bar", 3).Rets(3), Args("精灵", 0).Rets(3), Args("精灵", 3).Rets(6), Args("精灵", 6).Rets(6), ) } func TestMoveDotSOLEOL(t *testing.T) { buffer := "abc\ndef" // Index: // 012 34567 tt.Test(t, moveDotSOL, Args(buffer, 0).Rets(0), Args(buffer, 1).Rets(0), Args(buffer, 2).Rets(0), Args(buffer, 3).Rets(0), Args(buffer, 4).Rets(4), Args(buffer, 5).Rets(4), Args(buffer, 6).Rets(4), Args(buffer, 7).Rets(4), ) tt.Test(t, moveDotEOL, Args(buffer, 0).Rets(3), Args(buffer, 1).Rets(3), Args(buffer, 2).Rets(3), Args(buffer, 3).Rets(3), Args(buffer, 4).Rets(7), Args(buffer, 5).Rets(7), Args(buffer, 6).Rets(7), 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, moveDotUp, Args(buffer, 0).Rets(0), // a -> a Args(buffer, 1).Rets(1), // b -> b Args(buffer, 2).Rets(2), // c -> c Args(buffer, 3).Rets(3), // EOL1 -> EOL1 Args(buffer, 4).Rets(0), // 精 -> a Args(buffer, 7).Rets(2), // 灵 -> c Args(buffer, 10).Rets(3), // 语 -> EOL1 Args(buffer, 13).Rets(3), // EOL2 -> EOL1 Args(buffer, 14).Rets(4), // d -> 精 Args(buffer, 15).Rets(4), // e -> 精 (jump left half width) Args(buffer, 16).Rets(7), // f -> 灵 Args(buffer, 17).Rets(7), // EOL3 -> 灵 (jump left half width) ) tt.Test(t, moveDotDown, Args(buffer, 0).Rets(4), // a -> 精 Args(buffer, 1).Rets(4), // b -> 精 (jump left half width) Args(buffer, 2).Rets(7), // c -> 灵 Args(buffer, 3).Rets(7), // EOL1 -> 灵 (jump left half width) Args(buffer, 4).Rets(14), // 精 -> d Args(buffer, 7).Rets(16), // 灵 -> f Args(buffer, 10).Rets(17), // 语 -> EOL3 Args(buffer, 13).Rets(17), // EOL2 -> EOL3 Args(buffer, 14).Rets(14), // d -> d Args(buffer, 15).Rets(15), // e -> e Args(buffer, 16).Rets(16), // f -> f 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.Case{ Args(wordMoveTestBuffer, 0).Rets(0), Args(wordMoveTestBuffer, 1).Rets(0), Args(wordMoveTestBuffer, 2).Rets(0), Args(wordMoveTestBuffer, 3).Rets(0), Args(wordMoveTestBuffer, 4).Rets(3), Args(wordMoveTestBuffer, 16).Rets(3), Args(wordMoveTestBuffer, 19).Rets(16), Args(wordMoveTestBuffer, 23).Rets(19), Args(wordMoveTestBuffer, 40).Rets(23), } moveDotRightWordTests = []*tt.Case{ Args(wordMoveTestBuffer, 0).Rets(3), Args(wordMoveTestBuffer, 1).Rets(3), Args(wordMoveTestBuffer, 2).Rets(3), Args(wordMoveTestBuffer, 3).Rets(16), Args(wordMoveTestBuffer, 16).Rets(19), Args(wordMoveTestBuffer, 19).Rets(23), Args(wordMoveTestBuffer, 23).Rets(40), } // small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37 moveDotLeftSmallWordTests = []*tt.Case{ Args(wordMoveTestBuffer, 0).Rets(0), Args(wordMoveTestBuffer, 1).Rets(0), Args(wordMoveTestBuffer, 2).Rets(0), Args(wordMoveTestBuffer, 3).Rets(0), Args(wordMoveTestBuffer, 4).Rets(3), Args(wordMoveTestBuffer, 5).Rets(3), Args(wordMoveTestBuffer, 14).Rets(5), Args(wordMoveTestBuffer, 16).Rets(14), Args(wordMoveTestBuffer, 19).Rets(16), Args(wordMoveTestBuffer, 20).Rets(19), Args(wordMoveTestBuffer, 23).Rets(20), Args(wordMoveTestBuffer, 32).Rets(23), Args(wordMoveTestBuffer, 33).Rets(32), Args(wordMoveTestBuffer, 37).Rets(33), Args(wordMoveTestBuffer, 40).Rets(37), } moveDotRightSmallWordTests = []*tt.Case{ Args(wordMoveTestBuffer, 0).Rets(3), Args(wordMoveTestBuffer, 1).Rets(3), Args(wordMoveTestBuffer, 2).Rets(3), Args(wordMoveTestBuffer, 3).Rets(5), Args(wordMoveTestBuffer, 5).Rets(14), Args(wordMoveTestBuffer, 14).Rets(16), Args(wordMoveTestBuffer, 16).Rets(19), Args(wordMoveTestBuffer, 19).Rets(20), Args(wordMoveTestBuffer, 20).Rets(23), Args(wordMoveTestBuffer, 23).Rets(32), Args(wordMoveTestBuffer, 32).Rets(33), Args(wordMoveTestBuffer, 33).Rets(37), Args(wordMoveTestBuffer, 37).Rets(40), } // alnum-word boundaries: 0 5 16 20 23 33 moveDotLeftAlnumWordTests = []*tt.Case{ Args(wordMoveTestBuffer, 0).Rets(0), Args(wordMoveTestBuffer, 1).Rets(0), Args(wordMoveTestBuffer, 2).Rets(0), Args(wordMoveTestBuffer, 3).Rets(0), Args(wordMoveTestBuffer, 4).Rets(0), Args(wordMoveTestBuffer, 5).Rets(0), Args(wordMoveTestBuffer, 6).Rets(5), Args(wordMoveTestBuffer, 16).Rets(5), Args(wordMoveTestBuffer, 20).Rets(16), Args(wordMoveTestBuffer, 23).Rets(20), Args(wordMoveTestBuffer, 33).Rets(23), Args(wordMoveTestBuffer, 40).Rets(33), } moveDotRightAlnumWordTests = []*tt.Case{ Args(wordMoveTestBuffer, 0).Rets(5), Args(wordMoveTestBuffer, 1).Rets(5), Args(wordMoveTestBuffer, 2).Rets(5), Args(wordMoveTestBuffer, 3).Rets(5), Args(wordMoveTestBuffer, 4).Rets(5), Args(wordMoveTestBuffer, 5).Rets(16), Args(wordMoveTestBuffer, 16).Rets(20), Args(wordMoveTestBuffer, 20).Rets(23), Args(wordMoveTestBuffer, 23).Rets(33), Args(wordMoveTestBuffer, 33).Rets(40), } ) func TestMoveDotWord(t *testing.T) { tt.Test(t, moveDotLeftWord, moveDotLeftWordTests...) tt.Test(t, moveDotRightWord, moveDotRightWordTests...) } func TestMoveDotSmallWord(t *testing.T) { tt.Test(t, moveDotLeftSmallWord, moveDotLeftSmallWordTests...) tt.Test(t, moveDotRightSmallWord, moveDotRightSmallWordTests...) } func TestMoveDotAlnumWord(t *testing.T) { tt.Test(t, moveDotLeftAlnumWord, moveDotLeftAlnumWordTests...) tt.Test(t, moveDotRightAlnumWord, moveDotRightAlnumWordTests...) } elvish-0.21.0/pkg/edit/command_api.d.elv000066400000000000000000000005221465720375400200070ustar00rootroot00000000000000# Key bindings for command mode. This is currently a very small subset of Vi # command mode bindings. # # See also [`edit:command:start`](). var command:binding # Enter command mode. This mode is intended to emulate Vi's command mode, but # it is very incomplete right now. # # See also [`$edit:command:binding`](). fn command:start { } elvish-0.21.0/pkg/edit/command_api.go000066400000000000000000000011011465720375400173760ustar00rootroot00000000000000package edit // Implementation of the editor "command" mode. import ( "src.elv.sh/pkg/cli/modes" "src.elv.sh/pkg/eval" ) 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]any{ "start": func() { w := modes.NewStub(modes.StubSpec{ Bindings: bindings, Name: " COMMAND ", }) ed.app.PushAddon(w) }, })) } elvish-0.21.0/pkg/edit/command_api_test.go000066400000000000000000000010311465720375400204370ustar00rootroot00000000000000package 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, `set 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.21.0/pkg/edit/complete/000077500000000000000000000000001465720375400164175ustar00rootroot00000000000000elvish-0.21.0/pkg/edit/complete/complete.go000066400000000000000000000057651465720375400205730ustar00rootroot00000000000000// 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/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/np" ) // 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 { // A function for filtering raw candidates. If nil, no filtering is done. Filterer Filterer // Used to generate candidates for a command argument. Defaults to // GenerateFileNames. 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 } // 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, ev *eval.Evaler, cfg Config) (*Result, error) { 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{}) path := np.FindLeft(tree.Root, code.Dot) if len(path) == 0 { // This can happen when there is a parse error. return nil, errNoCompletion } for _, completer := range completers { ctx, rawItems, err := completer(path, ev, cfg) if err == errNoCompletion { continue } rawItems = cfg.Filterer(ctx.name, ctx.seed, rawItems) sort.Slice(rawItems, func(i, j int) bool { return rawItems[i].String() < rawItems[j].String() }) items := make([]modes.CompletionItem, len(rawItems)) for i, rawCand := range rawItems { items[i] = rawCand.Cook(ctx.quote) } 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.21.0/pkg/edit/complete/complete_test.go000066400000000000000000000320421465720375400216160ustar00rootroot00000000000000package complete // Mocked builtin commands import ( "fmt" "os" "runtime" "sort" "strings" "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/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) var Args = tt.Args 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: ""}, }, }) testutil.Set(t, &eachExternal, func(f func(string)) { f("external-cmd1") f("external-cmd2") }) testutil.Set(t, &environ, func() []string { return []string{"ENV1=", "ENV2="} }) ev := eval.NewEvaler() err := ev.Eval(parse.SourceForTest(strings.Join([]string{ "var local-var1 = $nil", "var local-var2 = $nil", "fn local-fn1 { }", "fn local-fn2 { }", "var local-ns1: = (ns [&lorem=$nil])", "var local-ns2: = (ns [&ipsum=$nil])", }, "\n")), eval.EvalCfg{}) if err != nil { t.Fatalf("evaler setup: %v", err) } ev.ReplaceBuiltin( eval.BuildNs(). AddVar("builtin-var1", vars.NewReadOnly(nil)). AddVar("builtin-var2", vars.NewReadOnly(nil)). AddGoFn("builtin-fn1", func() {}). AddGoFn("builtin-fn2", func() {}). Ns()) var cfg Config cfg = Config{ Filterer: FilterPrefix, ArgGenerator: func(args []string) ([]RawItem, error) { if len(args) >= 2 && args[0] == "sudo" { return GenerateForSudo(args, ev, cfg) } return GenerateFileNames(args) }, } argGeneratorDebugCfg := Config{ 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{ ArgGenerator: func([]string) ([]RawItem, error) { return []RawItem{PlainItem("a"), PlainItem("b"), PlainItem("a")}, nil }, } allFileNameItems := []modes.CompletionItem{ fci("a.exe", " "), fci("d"+string(os.PathSeparator), ""), fci("non-exe", " "), } allCommandItems := []modes.CompletionItem{ ci("builtin-fn1"), ci("builtin-fn2"), ci("external-cmd1"), ci("external-cmd2"), ci("local-fn1"), ci("local-fn2"), ci("local-ns1:"), ci("local-ns2:"), } // Add all special commands. for name := range eval.IsBuiltinSpecial { allCommandItems = append(allCommandItems, ci(name)) } sort.Slice(allCommandItems, func(i, j int) bool { return allCommandItems[i].ToInsert < allCommandItems[j].ToInsert }) tt.Test(t, Complete, // Candidates are deduplicated. Args(cb("ls "), ev, dupCfg).Rets( &Result{ Name: "argument", Replace: r(3, 3), Items: []modes.CompletionItem{ ci("a"), ci("b"), }, }, nil), // Complete arguments using GenerateFileNames. Args(cb("ls "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(3, 3), Items: allFileNameItems}, nil), Args(cb("ls a"), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(3, 4), Items: []modes.CompletionItem{fci("a.exe", " ")}}, nil), // GenerateForSudo completing external commands. Args(cb("sudo "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(5, 5), Items: []modes.CompletionItem{ci("external-cmd1"), ci("external-cmd2")}}, nil), // GenerateForSudo completing non-command arguments. Args(cb("sudo ls "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(8, 8), Items: allFileNameItems}, nil), // Custom arg completer, new argument Args(cb("ls a "), ev, argGeneratorDebugCfg).Rets( &Result{ Name: "argument", Replace: r(5, 5), Items: []modes.CompletionItem{ci(`[]string{"ls", "a", ""}`)}}, nil), Args(cb("ls a b"), ev, argGeneratorDebugCfg).Rets( &Result{ Name: "argument", Replace: r(5, 6), Items: []modes.CompletionItem{ci(`[]string{"ls", "a", "b"}`)}}, nil), // Complete for special command "set". Args(cb("set "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(4, 4), Items: []modes.CompletionItem{ ci("builtin-fn1~"), ci("builtin-fn2~"), ci("builtin-var1"), ci("builtin-var2"), ci("local-fn1~"), ci("local-fn2~"), ci("local-ns1:"), ci("local-ns2:"), ci("local-var1"), ci("local-var2"), }, }), Args(cb("set @"), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(4, 5), Items: []modes.CompletionItem{ ci("@builtin-fn1~"), ci("@builtin-fn2~"), ci("@builtin-var1"), ci("@builtin-var2"), ci("@local-fn1~"), ci("@local-fn2~"), ci("@local-ns1:"), ci("@local-ns2:"), ci("@local-var1"), ci("@local-var2"), }, }), Args(cb("set local-ns1:"), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(4, 14), Items: []modes.CompletionItem{ ci("local-ns1:lorem"), }, }), // Completing an argument after "=" use the default generator (in this // case filenames). Args(cb("set a = "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(8, 8), Items: allFileNameItems, }), // But completing the "=" itself offers no candidates. Args(cb("set a ="), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(6, 7), Items: nil, }), // "tmp" has the same completer. Args(cb("tmp "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(4, 4), Items: []modes.CompletionItem{ ci("builtin-fn1~"), ci("builtin-fn2~"), ci("builtin-var1"), ci("builtin-var2"), ci("local-fn1~"), ci("local-fn2~"), ci("local-ns1:"), ci("local-ns2:"), ci("local-var1"), ci("local-var2"), }, }), // "del" has a similar completer. Args(cb("del "), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(4, 4), Items: []modes.CompletionItem{ ci("local-fn1~"), ci("local-fn2~"), ci("local-ns1:"), ci("local-ns2:"), ci("local-var1"), ci("local-var2"), }, }), // Complete commands at an empty buffer, generating special forms, // externals, functions, namespaces and variable assignments. Args(cb(""), ev, cfg).Rets( &Result{Name: "command", Replace: r(0, 0), Items: allCommandItems}, nil), // Complete at an empty closure. Args(cb("{ "), ev, cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a newline. Args(cb("a\n"), ev, cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a semicolon. Args(cb("a;"), ev, cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete after a pipe. Args(cb("a|"), ev, cfg).Rets( &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, nil), // Complete at the beginning of output capture. Args(cb("a ("), ev, cfg).Rets( &Result{Name: "command", Replace: r(3, 3), Items: allCommandItems}, nil), // Complete at the beginning of exception capture. Args(cb("a ?("), ev, cfg).Rets( &Result{Name: "command", Replace: r(4, 4), Items: allCommandItems}, nil), // Complete external commands with the e: prefix. Args(cb("e:"), ev, cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: []modes.CompletionItem{ ci("e:external-cmd1"), ci("e:external-cmd2"), }}, nil), // Commands newly defined by fn are supported too. Args(cb("fn new-fn { }; new-"), ev, cfg).Rets( &Result{ Name: "command", Replace: r(15, 19), Items: []modes.CompletionItem{ci("new-fn")}}, nil), // TODO(xiaq): Add tests for completing indices. // Complete filenames for redirection. Args(cb("p >"), ev, cfg).Rets( &Result{Name: "redir", Replace: r(3, 3), Items: allFileNameItems}, nil), Args(cb("p > a"), ev, cfg).Rets( &Result{ Name: "redir", Replace: r(4, 5), Items: []modes.CompletionItem{fci("a.exe", " ")}}, nil), // Completing variables. // All variables. Args(cb("p $"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(3, 3), Items: []modes.CompletionItem{ ci("E:"), ci("builtin-fn1~"), ci("builtin-fn2~"), ci("builtin-var1"), ci("builtin-var2"), ci("e:"), ci("local-fn1~"), ci("local-fn2~"), ci("local-ns1:"), ci("local-ns2:"), ci("local-var1"), ci("local-var2"), }}, nil), // Variables with a prefix. Args(cb("p $local-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(3, 9), Items: []modes.CompletionItem{ ci("local-fn1~"), ci("local-fn2~"), ci("local-ns1:"), ci("local-ns2:"), ci("local-var1"), ci("local-var2"), }}, nil), // Variables newly defined in the code, in the current scope. Args(cb("var new-var; p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(16, 20), Items: []modes.CompletionItem{ci("new-var")}}, nil), // Sigils in "var" are not part of the variable name. Args(cb("var @new-var = a b; p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(23, 27), Items: []modes.CompletionItem{ci("new-var")}}, nil), // Function parameters are recognized as newly defined variables too. Args(cb("{ |new-var| p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(15, 19), Items: []modes.CompletionItem{ci("new-var")}}, nil), // Variables newly defined in the code, in an outer scope. Args(cb("var new-var; { p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(18, 22), Items: []modes.CompletionItem{ci("new-var")}}, nil), // Variables newly defined in the code, but in a scope not visible from // the point of completion, are not included. Args(cb("{ var new-var } p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(19, 23), Items: nil, }, nil), // Variables defined by fn are supported too. Args(cb("fn new-fn { }; p $new-"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(18, 22), Items: []modes.CompletionItem{ci("new-fn~")}}, nil), // Variables in a namespace. // 01234567890123 Args(cb("p $local-ns1:"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(13, 13), Items: []modes.CompletionItem{ci("lorem")}}, nil), // Variables in the special e: namespace. // 012345 Args(cb("p $e:"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(5, 5), Items: []modes.CompletionItem{ ci("external-cmd1~"), ci("external-cmd2~"), }}, nil), // Variable in the special E: namespace. // 012345 Args(cb("p $E:"), ev, cfg).Rets( &Result{ Name: "variable", Replace: r(5, 5), Items: []modes.CompletionItem{ ci("ENV1"), ci("ENV2"), }}, nil), // Variables in a nonexistent namespace. // 01234567 Args(cb("p $bad:"), ev, cfg).Rets( &Result{Name: "variable", Replace: r(7, 7)}, nil), // Variables in a nested nonexistent namespace. // 0123456789012345678901 Args(cb("p $local-ns1:bad:bad:"), ev, cfg).Rets( &Result{Name: "variable", Replace: r(21, 21)}, nil), // No completion in supported context. Args(cb("nop ["), ev, cfg).Rets((*Result)(nil), errNoCompletion), // No completion after parse error. Args(cb("nop `"), ev, cfg).Rets((*Result)(nil), errNoCompletion), ) // Completions of filename involving symlinks and local commands. if runtime.GOOS == "windows" { // Symlinks require admin permissions on Windows, so we won't test them // Completing local commands after forward slash tt.Test(t, Complete, // Complete local external commands. Args(cb("./"), ev, cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: []modes.CompletionItem{ fci("./a.exe", " "), fci(`./d\`, "")}, }, nil), ) // Completing local commands after backslash tt.Test(t, Complete, // Complete local external commands. Args(cb(`.\`), ev, cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: []modes.CompletionItem{ fci(`.\a.exe`, " "), fci(`.\d\`, "")}, }, nil), ) } else { err := os.Symlink("d", "d2") if err != nil { panic(err) } allLocalCommandItems := []modes.CompletionItem{ fci("./a.exe", " "), fci("./d/", ""), fci("./d2/", ""), } tt.Test(t, Complete, // Filename completion treats symlink to directories as directories. // 01234 Args(cb("p > d"), ev, cfg).Rets( &Result{ Name: "redir", Replace: r(4, 5), Items: []modes.CompletionItem{fci("d/", ""), fci("d2/", "")}}, nil, ), // Complete local external commands. Args(cb("./"), ev, cfg).Rets( &Result{ Name: "command", Replace: r(0, 2), Items: allLocalCommandItems}, nil), // After sudo. Args(cb("sudo ./"), ev, cfg).Rets( &Result{ Name: "argument", Replace: r(5, 7), Items: allLocalCommandItems}, nil), ) } } func cb(s string) CodeBuffer { return CodeBuffer{s, len(s)} } func ci(s string) modes.CompletionItem { return modes.CompletionItem{ToShow: ui.T(s), ToInsert: s} } func fci(s, suffix string) modes.CompletionItem { return modes.CompletionItem{ ToShow: ui.T(s, ui.StylingFromSGR(lscolors.GetColorist().GetStyle(s))), ToInsert: parse.Quote(s) + suffix} } func r(i, j int) diag.Ranging { return diag.Ranging{From: i, To: j} } elvish-0.21.0/pkg/edit/complete/completers.go000066400000000000000000000135331465720375400211300ustar00rootroot00000000000000package complete import ( "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/np" ) var completers = []func(np.Path, *eval.Evaler, Config) (*context, []RawItem, error){ completeCommand, completeIndex, completeRedir, completeVariable, completeArg, } type context struct { name string seed string quote parse.PrimaryType interval diag.Ranging } func completeArg(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) { var form *parse.Form if p.Match(np.Sep, np.Store(&form)) && form.Head != nil { // Case 1: starting a new argument. ctx := &context{"argument", "", parse.Bareword, range0(p[0].Range().To)} args := purelyEvalForm(form, "", p[0].Range().To, ev) items, err := generateArgs(args, ev, p, cfg) return ctx, items, err } var expr np.SimpleExprData if p.Match(np.SimpleExpr(&expr, ev), np.Store(&form)) && form.Head != nil && form.Head != expr.Compound { // Case 2: in an incomplete argument. ctx := &context{"argument", expr.Value, expr.PrimarType, expr.Compound.Range()} args := purelyEvalForm(form, expr.Value, expr.Compound.Range().From, ev) items, err := generateArgs(args, ev, p, cfg) return ctx, items, err } return nil, nil, errNoCompletion } func completeCommand(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) { generateForEmpty := func(pos int) (*context, []RawItem, error) { ctx := &context{"command", "", parse.Bareword, range0(pos)} items, err := generateCommands("", ev, p) return ctx, items, err } if p.Match(np.Chunk) { // 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(p[0].Range().To) } if p.Match(np.Sep, np.Chunk) || p.Match(np.Sep, np.Pipeline) { // Case 2: Just after a newline, semicolon, or a pipe. return generateForEmpty(p[0].Range().To) } var primary *parse.Primary if p.Match(np.Sep, np.Store(&primary)) { t := primary.Type if t == parse.OutputCapture || t == parse.ExceptionCapture || t == parse.Lambda { // Case 3: At the beginning of output, exception capture or lambda. // // TODO: Don't trigger after "{|". return generateForEmpty(p[0].Range().To) } } var expr np.SimpleExprData var form *parse.Form if p.Match(np.SimpleExpr(&expr, ev), np.Store(&form)) && form.Head == expr.Compound { // Case 4: At an already started command. ctx := &context{"command", expr.Value, expr.PrimarType, expr.Compound.Range()} items, err := generateCommands(expr.Value, ev, p) 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(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) { generateForEmpty := func(v any, pos int) (*context, []RawItem, error) { ctx := &context{"index", "", parse.Bareword, range0(pos)} return ctx, generateIndices(v), nil } var indexing *parse.Indexing if p.Match(np.Sep, np.Store(&indexing)) || p.Match(np.Sep, np.Array, np.Store(&indexing)) { // We are at a new index, either directly after the opening bracket, or // after an existing index and some spaces. if len(indexing.Indices) == 1 { if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil { return generateForEmpty(indexee, p[0].Range().To) } } } var expr np.SimpleExprData if p.Match(np.SimpleExpr(&expr, ev), np.Array, np.Store(&indexing)) { // We are just after an incomplete index. if len(indexing.Indices) == 1 { if indexee := ev.PurelyEvalPrimary(indexing.Head); indexee != nil { ctx := &context{ "index", expr.Value, expr.PrimarType, expr.Compound.Range()} return ctx, generateIndices(indexee), nil } } } return nil, nil, errNoCompletion } func completeRedir(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) { if p.Match(np.Sep, np.Redir) { // Empty redirection target. ctx := &context{"redir", "", parse.Bareword, range0(p[0].Range().To)} items, err := generateFileNames("", nil) return ctx, items, err } var expr np.SimpleExprData if p.Match(np.SimpleExpr(&expr, ev), np.Redir) { // Non-empty redirection target. ctx := &context{"redir", expr.Value, expr.PrimarType, expr.Compound.Range()} items, err := generateFileNames(expr.Value, nil) return ctx, items, err } return nil, nil, errNoCompletion } func completeVariable(p np.Path, ev *eval.Evaler, cfg Config) (*context, []RawItem, error) { primary, ok := p[0].(*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 eachVariableInNs(ev, p, ns, func(varname string) { items = append(items, noQuoteItem(parse.QuoteVariableName(varname))) }) if ns == "" { items = append(items, noQuoteItem("e:"), noQuoteItem("E:")) } return ctx, items, nil } func purelyEvalForm(form *parse.Form, seed string, upto int, ev *eval.Evaler) []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 } func range0(pos int) diag.Ranging { return diag.Ranging{From: pos, To: pos} } elvish-0.21.0/pkg/edit/complete/filterers.go000066400000000000000000000005441465720375400207500ustar00rootroot00000000000000package 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.21.0/pkg/edit/complete/generators.go000066400000000000000000000131711465720375400211220ustar00rootroot00000000000000package complete import ( "fmt" "io/fs" "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/parse" "src.elv.sh/pkg/parse/np" "src.elv.sh/pkg/ui" ) const pathSeparator = string(filepath.Separator) var eachExternal = fsutil.EachExternal // 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) { if len(args) == 0 { return nil, nil } return generateFileNames(args[len(args)-1], nil) } // GenerateFileNames returns directory name candidates that are suitable for // completing the last argument. It can be used in Config.ArgGenerator. func GenerateDirNames(args []string) ([]RawItem, error) { if len(args) == 0 { return nil, nil } return generateFileNames(args[len(args)-1], fs.FileInfo.IsDir) } // GenerateForSudo generates candidates for sudo. func GenerateForSudo(args []string, ev *eval.Evaler, cfg Config) ([]RawItem, error) { switch { case len(args) < 2: return nil, errNoCompletion case len(args) == 2: // Complete external commands. return generateExternalCommands(args[1]) default: return cfg.ArgGenerator(args[1:]) } } // Internal generators, used from completers. func generateArgs(args []string, ev *eval.Evaler, p np.Path, cfg Config) ([]RawItem, error) { switch args[0] { case "set", "tmp": for i := 1; i < len(args); i++ { if args[i] == "=" { if i == len(args)-1 { // Completing the "=" itself; don't offer any candidates. return nil, nil } else { // Completing an argument after "="; fall back to the // default arg generator. return cfg.ArgGenerator(args) } } } seed := args[len(args)-1] sigil, qname := eval.SplitSigil(seed) ns, _ := eval.SplitIncompleteQNameNs(qname) var items []RawItem eachVariableInNs(ev, p, ns, func(varname string) { items = append(items, noQuoteItem(sigil+parse.QuoteVariableName(ns+varname))) }) return items, nil case "del": // This partially duplicates eachVariableInNs with ns = "", but we don't // offer builtin variables. var items []RawItem addItem := func(varname string) { items = append(items, noQuoteItem(parse.QuoteVariableName(varname))) } ev.Global().IterateKeysString(addItem) eachDefinedVariable(p[len(p)-1], p[0].Range().From, addItem) return items, nil } return cfg.ArgGenerator(args) } func generateExternalCommands(seed string) ([]RawItem, error) { if fsutil.DontSearch(seed) { // Completing a local external command name. return generateFileNames(seed, executableOrDir) } var items []RawItem eachExternal(func(s string) { items = append(items, PlainItem(s)) }) return items, nil } func generateCommands(seed string, ev *eval.Evaler, p np.Path) ([]RawItem, error) { if fsutil.DontSearch(seed) { // Completing a local external command name. return generateFileNames(seed, executableOrDir) } 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. eachExternal(func(command string) { addPlainItem("e:" + command) }) return cands, nil } // Generate all special forms. for name := range eval.IsBuiltinSpecial { addPlainItem(name) } // Generate all external commands (without the e: prefix). eachExternal(addPlainItem) sigil, qname := eval.SplitSigil(seed) ns, _ := eval.SplitIncompleteQNameNs(qname) if sigil == "" { // Generate functions, namespaces, and variable assignments. eachVariableInNs(ev, p, 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) } }) } return cands, nil } func generateFileNames(seed string, statPred func(fs.FileInfo) 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() stat, 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 } // Apply statPred if given. if statPred != nil && !statPred(stat) { continue } // Full filename for source and getStyle. full := dir + name // Will be set to an empty space for non-directories suffix := " " if stat.IsDir() { full += pathSeparator suffix = "" } else if stat.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, Display: ui.T(full, ui.StylingFromSGR(lsColor.GetStyle(full))), }) } return items, nil } func executableOrDir(stat fs.FileInfo) bool { return fsutil.IsExecutable(stat) || stat.IsDir() } func generateIndices(v any) []RawItem { var items []RawItem vals.IterateKeys(v, func(k any) 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.21.0/pkg/edit/complete/ns_helper.go000066400000000000000000000045551465720375400207360ustar00rootroot00000000000000package complete import ( "os" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" "src.elv.sh/pkg/parse/np" ) var environ = os.Environ // Calls f for each variable name in namespace ns that can be found at the point // of np. func eachVariableInNs(ev *eval.Evaler, p np.Path, ns string, f func(s string)) { switch ns { case "", ":": ev.Global().IterateKeysString(f) ev.Builtin().IterateKeysString(f) eachDefinedVariable(p[len(p)-1], p[0].Range().From, f) case "e:": eachExternal(func(cmd string) { f(cmd + eval.FnSuffix) }) case "E:": for _, s := range environ() { if i := strings.IndexByte(s, '='); i > 0 { f(s[:i]) } } default: // TODO: Support namespaces defined in the code too. segs := eval.SplitQNameSegs(ns) mod := ev.Global().IndexString(segs[0]) if mod == nil { mod = ev.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 f for each variables defined in n that are visible at pos. func eachDefinedVariable(n parse.Node, pos int, f func(string)) { if fn, ok := n.(*parse.Form); ok { eachDefinedVariableInForm(fn, f) } if pn, ok := n.(*parse.Primary); ok && pn.Type == parse.Lambda { for _, param := range pn.Elements { if varRef, ok := cmpd.StringLiteral(param); ok { _, name := eval.SplitSigil(varRef) f(name) } } } for _, ch := range parse.Children(n) { if ch.Range().From > pos { break } if pn, ok := ch.(*parse.Primary); ok && pn.Type == parse.Lambda { if pos >= pn.Range().To { continue } } eachDefinedVariable(ch, pos, f) } } // Calls f for each variable defined in fn. func eachDefinedVariableInForm(fn *parse.Form, f func(string)) { if fn.Head == nil { return } switch head, _ := cmpd.StringLiteral(fn.Head); head { case "var": for _, arg := range fn.Args { if parse.SourceText(arg) == "=" { break } // TODO: This simplified version may not match the actual // algorithm used by the compiler to parse an LHS. if varRef, ok := cmpd.StringLiteral(arg); ok { _, name := eval.SplitSigil(varRef) f(name) } } case "fn": if len(fn.Args) >= 1 { if name, ok := cmpd.StringLiteral(fn.Args[0]); ok { f(name + eval.FnSuffix) } } } } elvish-0.21.0/pkg/edit/complete/raw_item.go000066400000000000000000000026171465720375400205630ustar00rootroot00000000000000package 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: ui.T(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: ui.T(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 ui.Text // How the item is displayed. If empty, defaults to ui.T(Stem). } 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 == nil { display = ui.T(c.Stem) } return modes.CompletionItem{ ToInsert: quoted + c.CodeSuffix, ToShow: display, } } elvish-0.21.0/pkg/edit/complete_getopt.d.elv000066400000000000000000000060051465720375400207340ustar00rootroot00000000000000# 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 # ``` # # See also [`flag:parse-getopt`](). fn complete-getopt {|args opt-specs arg-handlers| } elvish-0.21.0/pkg/edit/complete_getopt.go000066400000000000000000000153421465720375400203350ustar00rootroot00000000000000package 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/ui" ) func completeGetopt(fm *eval.Frame, vArgs, vOpts, vArgHandlers any) 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: Make the Config field configurable _, parsedArgs, ctx := getopt.Complete(args, opts.opts, getopt.GNU) out := fm.ValueOutput() putShortOpt := func(opt *getopt.OptionSpec) error { c := complexItem{Stem: "-" + string(opt.Short)} if d, ok := opts.desc[opt]; ok { if e, ok := opts.argDesc[opt]; ok { c.Display = ui.T(c.Stem + " " + e + " (" + d + ")") } else { c.Display = ui.T(c.Stem + " (" + d + ")") } } return out.Put(c) } putLongOpt := func(opt *getopt.OptionSpec) error { c := complexItem{Stem: "--" + opt.Long} if d, ok := opts.desc[opt]; ok { if e, ok := opts.argDesc[opt]; ok { c.Display = ui.T(c.Stem + " " + e + " (" + d + ")") } else { c.Display = ui.T(c.Stem + " (" + d + ")") } } return out.Put(c) } call := func(fn eval.Callable, args ...any) error { return fn.Call(fm, args, eval.NoOpts) } switch ctx.Type { case getopt.OptionOrArgument, 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.AnyOption: 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.LongOption: for _, opt := range opts.opts { if opt.Long != "" && 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.Spec] if gen != nil { return call(gen, ctx.Option.Argument) } } return nil } // TODO(xiaq): Simplify most of the parsing below with reflection. func parseGetoptArgs(v any) ([]string, error) { var args []string var err error errIterate := vals.Iterate(v, func(v any) 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.OptionSpec desc map[*getopt.OptionSpec]string argDesc map[*getopt.OptionSpec]string argGenerator map[*getopt.OptionSpec]eval.Callable } func parseGetoptOptSpecs(v any) (parsedOptSpecs, error) { result := parsedOptSpecs{ nil, map[*getopt.OptionSpec]string{}, map[*getopt.OptionSpec]string{}, map[*getopt.OptionSpec]eval.Callable{}} var err error errIterate := vals.Iterate(v, func(v any) bool { m, ok := v.(vals.Map) if !ok { err = fmt.Errorf("opt should be map, got %s", vals.Kind(v)) return false } opt := &getopt.OptionSpec{} 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.Arity = getopt.RequiredArgument case argOptional: opt.Arity = 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 any) ([]eval.Callable, bool, error) { var argHandlers []eval.Callable var variadic bool var err error errIterate := vals.Iterate(v, func(v any) 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.21.0/pkg/edit/complete_getopt_test.elvts000066400000000000000000000130271465720375400221220ustar00rootroot00000000000000//each:complete-getopt-in-global /////////////////// # complete-getopt # /////////////////// ~> fn complete {|@args| var 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 }] ] var arg-handlers = [ {|_| put first1 first2 } {|_| put second1 second2 } ... ] complete-getopt $args $opt-specs $arg-handlers } // complete argument ~> complete '' ▶ first1 ▶ first2 ~> complete '' >&- Exception: port does not support value output [tty]:6:29-46: var arg-handlers = [ {|_| put first1 first2 } [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-15: complete '' >&- // complete option ~> complete - ▶ (edit:complex-candidate -a &code-suffix='' &display=[^styled '-a (Show all)']) ▶ (edit:complex-candidate --all &code-suffix='' &display=[^styled '--all (Show all)']) ▶ (edit:complex-candidate -n &code-suffix='' &display=[^styled '-n new-name (Set name)']) ▶ (edit:complex-candidate --name &code-suffix='' &display=[^styled '--name new-name (Set name)']) ~> complete - >&- Exception: port does not support value output [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-14: complete - >&- // complete long option ~> complete -- ▶ (edit:complex-candidate --all &code-suffix='' &display=[^styled '--all (Show all)']) ▶ (edit:complex-candidate --name &code-suffix='' &display=[^styled '--name new-name (Set name)']) ~> complete --a ▶ (edit:complex-candidate --all &code-suffix='' &display=[^styled '--all (Show all)']) ~> complete -- >&- Exception: port does not support value output [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-15: complete -- >&- // complete argument of short option ~> complete -n '' ▶ name1 ▶ name2 ~> complete -n '' >&- Exception: port does not support value output [tty]:5:39-54: &completer= {|_| put name1 name2 }] ] [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-18: complete -n '' >&- // complete argument of long option ~> complete --name '' ▶ name1 ▶ name2 ~> complete --name '' >&- Exception: port does not support value output [tty]:5:39-54: &completer= {|_| put name1 name2 }] ] [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-22: complete --name '' >&- // complete (normal) argument after option that doesn't take an argument ~> complete -a '' ▶ first1 ▶ first2 ~> complete -a '' >&- Exception: port does not support value output [tty]:6:29-46: var arg-handlers = [ {|_| put first1 first2 } [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-18: complete -a '' >&- // complete second argument ~> complete arg1 '' ▶ second1 ▶ second2 ~> complete arg1 '' >&- Exception: port does not support value output [tty]:7:29-48: {|_| put second1 second2 } ... ] [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-20: complete arg1 '' >&- // complete variadic argument ~> complete arg1 arg2 '' ▶ second1 ▶ second2 ~> complete arg1 arg2 '' >&- Exception: port does not support value output [tty]:7:29-48: {|_| put second1 second2 } ... ] [tty]:8:3-48: complete-getopt $args $opt-specs $arg-handlers [tty]:1:1-25: complete arg1 arg2 '' >&- # typechecks # ~> complete-getopt [foo []] [] [] Exception: arg should be string, got list [tty]:1:1-30: complete-getopt [foo []] [] [] ~> complete-getopt [] [foo] [] Exception: opt should be map, got string [tty]:1:1-27: complete-getopt [] [foo] [] ~> complete-getopt [] [[&short=[]]] [] Exception: short should be string, got list [tty]:1:1-35: complete-getopt [] [[&short=[]]] [] ~> complete-getopt [] [[&short=foo]] [] Exception: short should be exactly one rune, got foo [tty]:1:1-36: complete-getopt [] [[&short=foo]] [] ~> complete-getopt [] [[&long=[]]] [] Exception: long should be string, got list [tty]:1:1-34: complete-getopt [] [[&long=[]]] [] ~> complete-getopt [] [[&]] [] Exception: opt should have at least one of short and long forms [tty]:1:1-27: complete-getopt [] [[&]] [] ~> complete-getopt [] [[&short=a &arg-required=foo]] [] Exception: arg-required should be bool, got string [tty]:1:1-52: complete-getopt [] [[&short=a &arg-required=foo]] [] ~> complete-getopt [] [[&short=a &arg-optional=foo]] [] Exception: arg-optional should be bool, got string [tty]:1:1-52: complete-getopt [] [[&short=a &arg-optional=foo]] [] ~> complete-getopt [] [[&short=a &arg-required=$true &arg-optional=$true]] [] Exception: opt cannot have both arg-required and arg-optional [tty]:1:1-74: complete-getopt [] [[&short=a &arg-required=$true &arg-optional=$true]] [] ~> complete-getopt [] [[&short=a &desc=[]]] [] Exception: desc should be string, got list [tty]:1:1-43: complete-getopt [] [[&short=a &desc=[]]] [] ~> complete-getopt [] [[&short=a &arg-desc=[]]] [] Exception: arg-desc should be string, got list [tty]:1:1-47: complete-getopt [] [[&short=a &arg-desc=[]]] [] ~> complete-getopt [] [[&short=a &completer=[]]] [] Exception: completer should be fn, got list [tty]:1:1-48: complete-getopt [] [[&short=a &completer=[]]] [] ~> complete-getopt [] [] [foo] Exception: string except for ... not allowed as argument handler, got foo [tty]:1:1-27: complete-getopt [] [] [foo] ~> complete-getopt [] [] [[]] Exception: argument handler should be fn, got list [tty]:1:1-26: complete-getopt [] [] [[]] elvish-0.21.0/pkg/edit/completion.d.elv000066400000000000000000000076471465720375400177300ustar00rootroot00000000000000# A map containing argument completers. var completion:arg-completer # Keybinding for the completion mode. var completion:binding # A map mapping from context names to matcher functions. See the # [Matcher](#matcher) section. var completion:matcher # Produces a list of filenames that are suitable for completing the last # argument, ignoring all other arguments. The last argument is used in the # following ways: # # - The directory determines which directory to complete. # # - If the base name starts with `.`, it completes all files whose names start # with `.` (hidden files); otherwise it completes filenames that don't start # with `.` (non-hidden files). # # The rest of the base name is ignored; filtering is left to the # [matcher](#matcher). # # The outputs are [`edit:complex-candidate`]() objects, with styles determined # by `$E:LSCOLOR`. Directories have a trailing `/` in the stem; non-directory # files have a space as their code suffix. # # 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 # ~> ls -AR # ~/tmp/example> ls -AR # .ipsum .lorem bar d foo # # ./d: # bar foo # ~> edit:complete-filename '' # non-hidden files in working directory # ▶ (edit:complex-candidate bar &code-suffix=' ' &display=[^styled bar]) # ▶ (edit:complex-candidate d/ &code-suffix='' &display=[^styled (styled-segment d/ &fg-color=blue &bold)]) # ▶ (edit:complex-candidate foo &code-suffix=' ' &display=[^styled foo]) # ~> edit:complete-filename '.f' # hidden files in working directory # ▶ (edit:complex-candidate .ipsum &code-suffix=' ' &display=[^styled .ipsum]) # ▶ (edit:complex-candidate .lorem &code-suffix=' ' &display=[^styled .lorem]) # ~> edit:complete-filename ./d/f # non-hidden files in ./d # ▶ (edit:complex-candidate ./d/bar &code-suffix=' ' &display=[^styled ./d/bar]) # ▶ (edit:complex-candidate ./d/foo &code-suffix=' ' &display=[^styled ./d/foo]) # ``` fn complete-filename {|@args| } #doc:added-in 0.21 # # Like [`edit:complete-filename`](), but only generates directories. fn complete-dirname {|@args| } # 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. fn complex-candidate {|stem &display='' &code-suffix=''| } # 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 # } # ``` fn match-prefix {|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. fn match-subseq {|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 # } # ``` fn match-substr {|seed inputs?| } # Start the completion mode. fn completion:start { } # Starts the completion mode after accepting any pending autofix. # # If all the candidates share a non-empty prefix and that prefix starts with the # seed, inserts the prefix instead. fn completion:smart-start { } elvish-0.21.0/pkg/edit/completion.go000066400000000000000000000255051465720375400173160ustar00rootroot00000000000000package edit import ( "bufio" "fmt" "os" "reflect" "strings" "sync" "unicode/utf8" "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/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/ui" ) type complexCandidateOpts struct { CodeSuffix string Display any } func (*complexCandidateOpts) SetDefaultOptions() {} func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) (complexItem, error) { var display ui.Text switch displayOpt := opts.Display.(type) { case nil: // Leave display = nil case string: display = ui.T(displayOpt) case ui.Text: display = displayOpt default: return complexItem{}, errs.BadValue{What: "&display", Valid: "string or styled", Actual: vals.ReprPlain(displayOpt)} } return complexItem{ Stem: stem, CodeSuffix: opts.CodeSuffix, Display: display, }, nil } func completionStart(ed *Editor, bindings tk.Bindings, ev *eval.Evaler, cfg complete.Config, smart bool) { codeArea, ok := focusedCodeArea(ed.app) if !ok { return } if smart { ed.applyAutofix() } buf := codeArea.CopyState().Buffer result, err := complete.Complete( complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, ev, cfg) if err != nil { ed.app.Notify(modes.ErrorText(err)) 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(ed.app, modes.CompletionSpec{ Name: result.Name, Replace: result.Replace, Items: result.Items, Filter: filterSpec, Bindings: bindings, }) if w != nil { ed.app.PushAddon(w) } if err != nil { ed.app.Notify(modes.ErrorText(err)) } } 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{ 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(args, ev, cfg()) } nb.AddGoFns(map[string]any{ "complete-filename": wrapArgGenerator(complete.GenerateFileNames), "complete-dirname": wrapArgGenerator(complete.GenerateDirNames), "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]any{ "accept": func() { listingAccept(app) }, "smart-start": func() { completionStart(ed, bindings, ev, cfg(), true) }, "start": func() { completionStart(ed, bindings, ev, 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 any) (any, 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(any) bool) { vals.Feed(f, "stem", "code-suffix", "display") } func (c complexItem) Kind() string { return "map" } func (c complexItem) Equal(a any) bool { rhs, ok := a.(complexItem) return ok && c.Stem == rhs.Stem && c.CodeSuffix == rhs.CodeSuffix && reflect.DeepEqual(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)) // TODO: Add 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), vals.Repr(c.Display, indent+1)) } 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 any 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 any) { if errOut != nil { return } errOut = out.Put(m(strings.ToLower(vals.ToString(v)), seed)) }) } else { inputs(func(v any) { 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 any) 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.ValueCapturePort() if err != nil { nt.notifyf("cannot create pipe to run completion matcher: %v", err) return nil } err = ev.Call(matcher, eval.CallCfg{Args: []any{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([]any, 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 any) { 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 } elvish-0.21.0/pkg/edit/completion_test.elvts000066400000000000000000000032421465720375400210770ustar00rootroot00000000000000///////////////////// # complete-filename # ///////////////////// //complete-filename-in-global // Don't crash with no argument. Regression test for b.elv.sh/1799. ~> complete-filename ///////////////////// # complex-candidate # ///////////////////// //each:complex-candidate-in-global ## construction ## ~> complex-candidate a/b ▶ (edit:complex-candidate a/b &code-suffix='' &display=[^styled]) ~> complex-candidate a/b &code-suffix=' ' ▶ (edit:complex-candidate a/b &code-suffix=' ' &display=[^styled]) ~> complex-candidate a/b &code-suffix=' ' &display=A/B ▶ (edit:complex-candidate a/b &code-suffix=' ' &display=[^styled A/B]) ~> complex-candidate a/b &code-suffix=' ' &display=(styled A/B red) ▶ (edit:complex-candidate a/b &code-suffix=' ' &display=[^styled (styled-segment A/B &fg-color=red)]) ~> complex-candidate a/b &code-suffix=' ' &display=[] Exception: bad value: &display must be string or styled, but is [] [tty]:1:1-50: complex-candidate a/b &code-suffix=' ' &display=[] ## value operations ## ~> kind-of (complex-candidate stem) ▶ map ~> keys (complex-candidate stem) ▶ stem ▶ code-suffix ▶ display ~> repr (complex-candidate a/b &code-suffix=' ' &display=A/B) (edit:complex-candidate a/b &code-suffix=' ' &display=[^styled A/B]) ~> eq (complex-candidate stem) (complex-candidate stem) ▶ $true ~> eq (complex-candidate stem &code-suffix=' ') (complex-candidate stem) ▶ $false ~> eq (complex-candidate stem &display=STEM) (complex-candidate stem) ▶ $false ~> put [&(complex-candidate stem)=value][(complex-candidate stem)] ▶ value ~> put (complex-candidate a/b &code-suffix=' ' &display=A/B)[stem code-suffix display] ▶ a/b ▶ ' ' ▶ [^styled A/B] elvish-0.21.0/pkg/edit/completion_test.go000066400000000000000000000126021465720375400203470ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) 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 TestCompletionAddon_AppliesAutofix(t *testing.T) { f := setup(t) fooNs := eval.BuildNs().AddGoFn("a", func() {}).AddGoFn("b", func() {}).Ns() f.Evaler.AddModule("foo", fooNs) feedInput(f.TTYCtrl, "foo:") f.TestTTY(t, "~> foo:", Styles, " !!!!", term.DotHere, "\n", "Ctrl-A autofix: use foo Tab Enter autofix first", Styles, "++++++ +++ +++++", ) feedInput(f.TTYCtrl, "\t") f.TestTTY(t, "~> foo:a\n", Styles, " VVVVV", " COMPLETING command ", Styles, "******************** ", term.DotHere, "\n", "foo:a foo:b", Styles, "+++++ ", ) } func TestCompleteFilename(t *testing.T) { f := setup(t) testutil.ApplyDir(testutil.Dir{"d": testutil.Dir{"a": "", "b": ""}}) evals(f.Evaler, `var @cands = (edit:complete-filename ls ./d/a)`) testGlobal(t, f.Evaler, "cands", vals.MakeList( complexItem{Stem: "./d/a", CodeSuffix: " ", Display: ui.T("./d/a")}, complexItem{Stem: "./d/b", CodeSuffix: " ", Display: ui.T("./d/b")})) testThatOutputErrorIsBubbled(t, f, "edit:complete-filename ls ''") } 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, `var stem = (edit:complex-candidate stem)[stem]`) testGlobal(t, f.Evaler, "stem", "stem") } func TestCompletionArgCompleter_ArgsAndValueOutput(t *testing.T) { f := setup(t) evals(f.Evaler, `var foo-args = []`, `fn foo { }`, `set edit:completion:arg-completer[foo] = {|@args| set 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 { }`, `set 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 { }`, `set edit:completion:arg-completer[foo] = {|@args| echo val1 echo val2 }`, `var @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, `set 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, `var @prefix = (edit:match-prefix ab [ab abc cab acb ba [ab] [a b] [b a]])`, `var @substr = (edit:match-substr ab [ab abc cab acb ba [ab] [a b] [b a]])`, `var @subseq = (edit:match-subseq ab [ab abc cab acb ba [ab] [a b] [b a]])`, ) testGlobals(t, f.Evaler, map[string]any{ "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, `var @a = (edit:match-prefix &ignore-case ab [abc aBc AbC])`, `var @b = (edit:match-prefix &ignore-case aB [abc aBc AbC])`, `var @c = (edit:match-prefix &smart-case ab [abc aBc Abc])`, `var @d = (edit:match-prefix &smart-case aB [abc aBc AbC])`, ) testGlobals(t, f.Evaler, map[string]any{ "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.21.0/pkg/edit/config_api.d.elv000066400000000000000000000023031465720375400176350ustar00rootroot00000000000000# 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. var max-height # A list of functions to call before each readline cycle. Each function is # called without any arguments. var before-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. var after-readline # 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. var add-cmd-filters # Global keybindings, consulted for keys not handled by mode-specific bindings. # # See [Keybindings](#keybindings). var global-binding elvish-0.21.0/pkg/edit/config_api.go000066400000000000000000000072651465720375400172460ustar00rootroot00000000000000package 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" ) 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) } 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() { eval.CallHook(ev, nil, "$:before-readline", hook.Get().(vals.List)) }) } 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) { eval.CallHook(ev, nil, "$:after-readline", hook.Get().(vals.List), code) }) } 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. }) } 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 callFilters(ev *eval.Evaler, name string, filters vals.List, args ...any) 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 { complain("%s not function", name) continue } port1, collect, err := eval.ValueCapturePort() if err != nil { complain("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 { complain("%s return error", name) continue } if len(out) != 1 { complain("filter %s should only return $true or $false", name) continue } p, ok := out[0].(bool) if !ok { complain("filter %s should return bool", name) continue } if !p { return false } } return true } // TODO: This is not testable as it depends on stderr. Make it testable. func complain(format string, args ...any) { diag.ShowError(os.Stderr, fmt.Errorf(format, args...)) } 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.21.0/pkg/edit/config_api_test.go000066400000000000000000000070301465720375400202730ustar00rootroot00000000000000package 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( `var called = 0`, `set edit:before-readline = [ { set 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, `var called = 0`, `var called-with = ''`, `set edit:after-readline = [ {|code| set called = (+ $called 1); set 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]any{ "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: "set edit:add-cmd-filters = [{|_| put $true }]", input: "echo\n", wantHistory: []storedefs.Cmd{{Text: "echo", Seq: 1}}, }, { name: "callback outputs false", rc: "set edit:add-cmd-filters = [{|_| put $false }]", input: "echo\n", wantHistory: nil, }, { name: "false-true chain", rc: "set edit:add-cmd-filters = [{|_| put $false } {|_| put $true }]", input: "echo\n", wantHistory: nil, }, { name: "true-false chain", rc: "set edit:add-cmd-filters = [{|_| put $true } {|_| put $false }]", input: "echo\n", wantHistory: nil, }, { name: "positive", rc: "set edit:add-cmd-filters = [{|cmd| ==s $cmd echo }]", input: "echo\n", wantHistory: []storedefs.Cmd{{Text: "echo", Seq: 1}}, }, { name: "negative", rc: "set 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( `var called = $false`, `set @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`, `set 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.21.0/pkg/edit/editor.d.elv000066400000000000000000000002031465720375400170220ustar00rootroot00000000000000# A list of exceptions thrown from callbacks such as prompts. Useful for # examining tracebacks and other metadata. var exceptions elvish-0.21.0/pkg/edit/editor.go000066400000000000000000000105451465720375400164310ustar00rootroot00000000000000// 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" "sync/atomic" "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" "src.elv.sh/pkg/ui" ) // Editor is the interactive line editor for Elvish. type Editor struct { app cli.App ns *eval.Ns excMutex sync.RWMutex excList vals.List autofix atomic.Value // This is an ugly hack to let the implementation of edit:smart-enter and // edit:completion:smart-start to apply the autofix easily. This field is // set in initHighlighter. applyAutofix func() // 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 ...any) 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} ed.autofix.Store("") nb := eval.BuildNsNamed("edit") appSpec := cli.AppSpec{TTY: tty} hs, err := newHistStore(st) if err != nil { _ = err // TODO(xiaq): Report the error. } initMaxHeight(&appSpec, nb) initReadlineHooks(&appSpec, ev, nb) initAddCmdFilters(&appSpec, ev, nb, hs) initGlobalBindings(&appSpec, ed, ev, nb) initInsertAPI(&appSpec, ed, ev, nb) initHighlighter(&appSpec, ed, ev, nb) initPrompts(&appSpec, ed, ev, nb) ed.app = cli.NewApp(appSpec) initExceptionsAPI(ed, nb) initVarsAPI(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, nb) initStateAPI(ed.app, nb) initStoreAPI(ed.app, nb, hs) ed.ns = nb.Ns() initElvishState(ev, ed.ns) return ed } 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 ui.Text) { 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 ...any) { ed.app.Notify(ui.T(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.Conj(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.21.0/pkg/edit/editor_test.go000066400000000000000000000015071465720375400174660ustar00rootroot00000000000000package edit import ( "reflect" "testing" "src.elv.sh/pkg/store/storedefs" "src.elv.sh/pkg/ui" ) 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(ui.T("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.21.0/pkg/edit/filter/000077500000000000000000000000001465720375400160745ustar00rootroot00000000000000elvish-0.21.0/pkg/edit/filter/compile.go000066400000000000000000000054031465720375400200550ustar00rootroot00000000000000// 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/errutil" "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, errutil.Multi(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.21.0/pkg/edit/filter/compile_test.go000066400000000000000000000127431465720375400211210ustar00rootroot00000000000000package 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: [filter]:1:5: 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 { if err == nil { return noError } else if parse.UnpackErrors(err) != nil { return parseError } else { 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.21.0/pkg/edit/filter/filter.go000066400000000000000000000015451465720375400177150ustar00rootroot00000000000000package 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.21.0/pkg/edit/filter/highlight.go000066400000000000000000000030121465720375400203660ustar00rootroot00000000000000package 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, []ui.Text) { 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.21.0/pkg/edit/filter/highlight_test.go000066400000000000000000000021711465720375400214320ustar00rootroot00000000000000package 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.21.0/pkg/edit/highlight.d.elv000066400000000000000000000001151465720375400175050ustar00rootroot00000000000000# Executes the currently suggested [autofix](#autofix). fn apply-autofix { } elvish-0.21.0/pkg/edit/highlight.go000066400000000000000000000057311465720375400171130ustar00rootroot00000000000000package edit import ( "os" "os/exec" "strings" "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" "src.elv.sh/pkg/ui" ) func initHighlighter(appSpec *cli.AppSpec, ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { hl := highlight.NewHighlighter(highlight.Config{ Check: func(t parse.Tree) (string, []*eval.CompilationError) { autofixes, err := ev.CheckTree(t, nil) autofix := strings.Join(autofixes, "; ") ed.autofix.Store(autofix) return autofix, eval.UnpackCompilationErrors(err) }, HasCommand: func(cmd string) bool { return hasCommand(ev, cmd) }, AutofixTip: func(autofix string) ui.Text { return bindingTips(ed.ns, "insert:binding", bindingTip("autofix: "+autofix, "apply-autofix"), bindingTip("autofix first", "smart-enter", "completion:smart-start")) }, }) appSpec.Highlighter = hl ed.applyAutofix = func() { code := ed.autofix.Load().(string) if code == "" { return } // TODO: Check errors. // // For now, the autofix snippets are simple enough that we know they'll // always succeed. ev.Eval(parse.Source{Name: "[autofix]", Code: code}, eval.EvalCfg{}) hl.InvalidateCache() } nb.AddGoFn("apply-autofix", ed.applyAutofix) } 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() || fsutil.IsExecutable(stat)) } func hasExternalCommand(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } elvish-0.21.0/pkg/edit/highlight/000077500000000000000000000000001465720375400165565ustar00rootroot00000000000000elvish-0.21.0/pkg/edit/highlight/highlight.go000066400000000000000000000065661465720375400210710ustar00rootroot00000000000000// Package highlight provides an Elvish syntax highlighter. package highlight import ( "time" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" ) // Config keeps configuration for highlighting code. type Config struct { Check func(n parse.Tree) (string, []*eval.CompilationError) HasCommand func(name string) bool AutofixTip func(autofix string) ui.Text } // Information collected about a command region, used for asynchronous // highlighting. type cmdRegion struct { seg int cmd string } // Maximum wait time to block for late results. 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, []ui.Text) { var tips []ui.Text var errorRegions []region addDiagError := func(err error, r diag.Ranging, partial bool) { if partial { return } tips = append(tips, ui.T(err.Error())) errorRegions = append(errorRegions, region{ r.From, r.To, semanticRegion, errorRegion}) } tree, errParse := parse.Parse(parse.Source{Name: "[interactive]", Code: code}, parse.Config{}) for _, err := range parse.UnpackErrors(errParse) { addDiagError(err, err.Range(), err.Partial) } if cfg.Check != nil { autofix, diagErrors := cfg.Check(tree) for _, err := range diagErrors { addDiagError(err, err.Range(), err.Partial) } if autofix != "" && cfg.AutofixTip != nil { tips = append(tips, cfg.AutofixTip(autofix)) } } 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.Type == 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.Type] } 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, tips case <-time.After(maxBlockForLate): go func() { lateCb(<-lateCh) }() return text, tips } } return text, tips } elvish-0.21.0/pkg/edit/highlight/highlighter.go000066400000000000000000000030341465720375400214030ustar00rootroot00000000000000package 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 lates chan struct{} cacheMutex sync.Mutex cache cache } type cache struct { code string styledCode ui.Text tips []ui.Text } func NewHighlighter(cfg Config) *Highlighter { return &Highlighter{cfg: cfg, lates: make(chan struct{}, latesBufferSize)} } // Get returns the highlighted code and static errors found in the code as tips. func (hl *Highlighter) Get(code string) (ui.Text, []ui.Text) { hl.cacheMutex.Lock() defer hl.cacheMutex.Unlock() if code == hl.cache.code { return hl.cache.styledCode, hl.cache.tips } lateCb := func(styledCode ui.Text) { hl.cacheMutex.Lock() if hl.cache.code != code { // Late result was delivered after code has changed. Unlock and // return. hl.cacheMutex.Unlock() return } hl.cache.styledCode = styledCode // The channel send below might block, so unlock the state first. hl.cacheMutex.Unlock() hl.lates <- struct{}{} } styledCode, tips := highlight(code, hl.cfg, lateCb) hl.cache = cache{code, styledCode, tips} return styledCode, tips } // LateUpdates returns a channel for notifying late updates. func (hl *Highlighter) LateUpdates() <-chan struct{} { return hl.lates } // InvalidateCache invalidates the cached highlighting result. func (hl *Highlighter) InvalidateCache() { hl.cacheMutex.Lock() defer hl.cacheMutex.Unlock() hl.cache = cache{} } elvish-0.21.0/pkg/edit/highlight/highlighter_test.go000066400000000000000000000144711465720375400224510ustar00rootroot00000000000000package highlight import ( "reflect" "strings" "testing" "time" "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 any = anyMatcher{} var noTips []ui.Text 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. testutil.Set(t, &maxBlockForLate, testutil.Scaled(100*time.Millisecond)) hl := NewHighlighter(Config{ HasCommand: func(name string) bool { return name == "ls" }, }) tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), Args("ls").Rets( ui.MarkLines( "ls", styles, "vv", ), noTips), Args(" ls\n").Rets( ui.MarkLines( " ls\n", styles, " vv"), noTips), Args("ls $x 'y'").Rets( ui.MarkLines( "ls $x 'y'", styles, "vv $$ '''"), noTips), // 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, " $$"), noTips, ), ) } func TestHighlighter_ParseErrors(t *testing.T) { hl := NewHighlighter(Config{}) tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), // Parse error is highlighted and returned Args("ls ]").Rets( ui.MarkLines( "ls ]", styles, "vv ?"), matchTexts("1:4")), // Multiple parse errors Args("ls $? ]").Rets( ui.MarkLines( "ls $? ]", styles, "vv $? ?"), matchTexts("1:5", "1:7")), // Errors at the end are ignored Args("ls $").Rets(any, noTips), Args("ls [").Rets(any, noTips), ) } func TestHighlighter_AutofixesAndCheckErrors(t *testing.T) { ev := eval.NewEvaler() ev.AddModule("mod1", &eval.Ns{}) hl := NewHighlighter(Config{ Check: func(t parse.Tree) (string, []*eval.CompilationError) { autofixes, err := ev.CheckTree(t, nil) return strings.Join(autofixes, "; "), eval.UnpackCompilationErrors(err) }, AutofixTip: func(s string) ui.Text { return ui.T("autofix: " + s) }, }) tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), // Check error is highlighted and returned Args("ls $a ").Rets( ui.MarkLines( "ls $a ", styles, "vv ?? "), matchTexts("1:4")), // Multiple check errors Args("ls $a $b ").Rets( ui.MarkLines( "ls $a $b ", styles, "vv ?? ?? "), matchTexts("1:4", "1:7")), // Check errors at the end are ignored Args("set _").Rets(any, noTips), // Autofix Args("nop $mod1:").Rets( ui.MarkLines( "nop $mod1:", styles, "vvv $$$$$$"), matchTexts( "autofix: use mod1", // autofix )), ) } 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. testutil.Set(t, &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. testutil.Set(t, &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. testutil.Set(t, &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 textsMatcher struct{ substrings []string } func matchTexts(s ...string) textsMatcher { return textsMatcher{s} } func (m textsMatcher) Match(v tt.RetValue) bool { texts := v.([]ui.Text) if len(texts) != len(m.substrings) { return false } for i, text := range texts { if !strings.Contains(text.String(), m.substrings[i]) { return false } } return true } elvish-0.21.0/pkg/edit/highlight/regions.go000066400000000000000000000151361465720375400205610ustar00rootroot00000000000000package highlight import ( "sort" "strings" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/cmpd" ) 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 Type == "comment". // // In semantic regions, this field takes a value from a fixed list (see // below). Type 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)) { // 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", "tmp": emitRegionsInAssign(n, f) case "del": emitRegionsInDel(n, f) case "if": emitRegionsInIf(n, f) case "for": emitRegionsInFor(n, f) case "try": emitRegionsInTry(n, f) } if isBarewordCompound(n.Head) { f(n.Head, semanticRegion, commandRegion) } } func emitRegionsInAssign(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 emitRegionsInDel(n *parse.Form, f func(parse.Node, regionKind, string)) { for _, arg := range n.Args { 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") || matchKW("catch") { if i+1 < len(n.Args) && isStringLiteral(n.Args[i+1]) { f(n.Args[i+1], semanticRegion, variableRegion) i += 3 } else { i += 2 } } if matchKW("else") { i += 2 } matchKW("finally") } func isStringLiteral(n *parse.Compound) bool { _, ok := cmpd.StringLiteral(n) return ok } 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.21.0/pkg/edit/highlight/regions_test.go000066400000000000000000000146721465720375400216240ustar00rootroot00000000000000package 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, getRegionsFromString, 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 }), // 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 "tmp" special command Args("tmp x = foo").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // tmp {4, 5, semanticRegion, variableRegion}, // x {6, 7, semanticRegion, keywordRegion}, // = {8, 11, lexicalRegion, barewordRegion}, // foo }), // The "del" special command Args("del x y").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // tmp {4, 5, semanticRegion, variableRegion}, // x {6, 7, semanticRegion, variableRegion}, // y }), // 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, "}"}, }), // Regression test for b.elv.sh/1358. Args("try { } except { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 14, semanticRegion, keywordRegion}, // except {15, 16, lexicalRegion, "{"}, {17, 18, lexicalRegion, "}"}, }), Args("try { } catch e { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 13, semanticRegion, keywordRegion}, // catch {14, 15, semanticRegion, variableRegion}, // e {16, 17, lexicalRegion, "{"}, {18, 19, lexicalRegion, "}"}, }), Args("try { } catch e { } else { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 13, semanticRegion, keywordRegion}, // catch {14, 15, semanticRegion, variableRegion}, // e {16, 17, lexicalRegion, "{"}, {18, 19, lexicalRegion, "}"}, {20, 24, semanticRegion, keywordRegion}, // else {25, 26, lexicalRegion, "{"}, {27, 28, lexicalRegion, "}"}, }), // Regression test for b.elv.sh/1358. Args("try { } catch { }").Rets([]region{ {0, 3, semanticRegion, commandRegion}, // try {4, 5, lexicalRegion, "{"}, {6, 7, lexicalRegion, "}"}, {8, 13, semanticRegion, keywordRegion}, // catch {14, 15, lexicalRegion, "{"}, {16, 17, 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.21.0/pkg/edit/highlight/theme.go000066400000000000000000000013211465720375400202040ustar00rootroot00000000000000package 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.21.0/pkg/edit/highlight_test.go000066400000000000000000000061351465720375400201510ustar00rootroot00000000000000package 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/testutil" "src.elv.sh/pkg/tt" "src.elv.sh/pkg/ui" ) // High-level sanity test. func TestHighlighter(t *testing.T) { f := setup(t) // Highlighting feedInput(f.TTYCtrl, "put $true") f.TestTTY(t, "~> put $true", Styles, " vvv $$$$$", term.DotHere, ) // Check errors feedInput(f.TTYCtrl, "x ") f.TestTTY(t, "~> put $truex ", Styles, " vvv ?????? ", term.DotHere, "\n", "compilation error: [interactive]:1:5-10: variable $truex not found", ) } func TestHighlighter_Autofix(t *testing.T) { f := setup(t) f.Evaler.AddModule("mod1", &eval.Ns{}) feedInput(f.TTYCtrl, "put $mod1:") f.TestTTY(t, "~> put $mod1:", Styles, " vvv $$$$$$", term.DotHere, "\n", "Ctrl-A autofix: use mod1 Tab Enter autofix first", Styles, "++++++ +++ +++++", ) f.TTYCtrl.Inject(term.K('A', ui.Ctrl)) f.TestTTY(t, "~> put $mod1:", Styles, " vvv $$$$$$", term.DotHere, ) } // Fine-grained tests against the highlighter. 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, hasCommand, // Builtin special form Args(ev, "if").Rets(true), // Builtin function Args(ev, "put").Rets(true), // User-defined function Args(ev, "good").Rets(true), // Function in modules Args(ev, "a:good").Rets(true), Args(ev, "a:b:good").Rets(true), Args(ev, "a:bad").Rets(false), Args(ev, "a:b:bad").Rets(false), // Non-searching directory and external Args(ev, "./a").Rets(true), Args(ev, "a/b").Rets(true), Args(ev, "a/b/c/executable").Rets(true), Args(ev, "./bad").Rets(false), Args(ev, "a/bad").Rets(false), // External in PATH Args(ev, "external").Rets(true), Args(ev, "@external").Rets(true), Args(ev, "ex:tern:al").Rets(colonInFilenameOk), // With explicit e: Args(ev, "e:external").Rets(true), Args(ev, "e:bad-external").Rets(false), // Non-existent Args(ev, "bad").Rets(false), Args(ev, "a:").Rets(false), ) } 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.21.0/pkg/edit/hist_store.go000066400000000000000000000025111465720375400173200ustar00rootroot00000000000000package 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.21.0/pkg/edit/histwalk.d.elv000066400000000000000000000012101465720375400173610ustar00rootroot00000000000000# ```elvish # edit:history:binding # ``` # # Binding table for the history mode. var history:binding # Starts the history mode. fn history:start { } # Walks to the previous entry in history mode. fn history:up { } # Walks to the next entry in history mode. fn history:down { } # Walks to the next entry in history mode, or quit the history mode if already # at the newest entry. fn history:down-or-quit { } # Import command history entries that happened after the current session # started. fn history:fast-forward { } # Replaces the content of the buffer with the current history mode entry, and # closes history mode. fn history:accept { } elvish-0.21.0/pkg/edit/histwalk.go000066400000000000000000000033021465720375400167620ustar00rootroot00000000000000package 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" ) 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]any{ "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) } }, "accept": func() { notifyError(app, histwalkDo(app, modes.Histwalk.Accept)) }, "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(modes.ErrorText(err)) } } elvish-0.21.0/pkg/edit/histwalk_test.go000066400000000000000000000035441465720375400200310ustar00rootroot00000000000000package 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, "error: end of history", Styles, "!!!!!!") } 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, "error: end of history", Styles, "!!!!!!") } func TestHistWalk_Accept(t *testing.T) { f := startHistwalkTest(t) evals(f.Evaler, `edit:history:accept; edit:redraw`) f.TestTTY(t, "~> echo a", Styles, " vvvv ", term.DotHere, ) } func TestHistWalk_ImplicitAccept(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.21.0/pkg/edit/init.elv000066400000000000000000000102051465720375400162600ustar00rootroot00000000000000set after-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| set command-duration = $m[duration] } ] set completion:arg-completer = [ &cd= $complete-dirname~ &sudo= $complete-sudo~ &doc:show= {|@a| use doc; doc:-symbols } &doc:source= {|@a| use doc; doc:-symbols } ] set global-binding = (binding-table [ &Ctrl-'['= $close-mode~ &Alt-x= $minibuf:start~ ]) set 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~ &Down= $end-of-history~ &Alt-Enter={ insert-at-dot "\n" } &Ctrl-A= $apply-autofix~ &Enter= $smart-enter~ &Ctrl-D= $return-eof~ ]) set 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~ &a= { $move-dot-right~; $close-mode~ } &j= $move-dot-down~ &k= $move-dot-up~ &l= $move-dot-right~ &w= $move-dot-right-word~ &x= $kill-rune-right~ ]) set listing:binding = (binding-table [ &Up= $listing:up~ &Down= $listing:down~ &PageUp= $listing:page-up~ &PageDown= $listing:page-down~ &Tab= $listing:down-cycle~ &Shift-Tab= $listing:up-cycle~ ]) set histlist:binding = (binding-table [ &Ctrl-D= $histlist:toggle-dedup~ ]) set 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~ ]) set 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~ ]) set history:binding = (binding-table [ &Up= $history:up~ &Down= $history:down-or-quit~ &Ctrl-'['= $close-mode~ ]) set lastcmd:binding = (binding-table [ &Alt-,= $listing:accept~ ]) set -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). set 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.21.0/pkg/edit/insert_api.d.elv000066400000000000000000000110101465720375400176670ustar00rootroot00000000000000# 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 # set edit:abbr['||'] = '| less' # set 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. # # See also [`$edit:command-abbr`]() and [`$edit:small-word-abbr`](). var abbr # A map from command abbreviations to their expansions. # # A command abbreviation is replaced by its expansion when seen in the command # position followed by a [whitespace](language.html#whitespace). This is # similar to the Fish shell's # [abbreviations](https://fishshell.com/docs/current/cmds/abbr.html), but does # not trigger when executing a command with Enter -- you must type a space # first. # # Examples: # # ```elvish # set edit:command-abbr['l'] = 'less' # set edit:command-abbr['gc'] = 'git commit' # ``` # # See also [`$edit:abbr`]() and [`$edit:small-word-abbr`](). var command-abbr # A map from small-word abbreviations to 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. See the description of # [small words](#word-types) for more information. # # As an example, with the following configuration: # # ```elvish # set 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 # set 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 # set edit:small-word-abbr['gcp'] = 'git cherry-pick -x' # set 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. # # See also [`$edit:abbr`]() [`$edit:command-abbr`](). var small-word-abbr # Toggles the value of [$edit:insert:quote-paste]. fn toggle-quote-paste { } # Binding map for the insert mode. # # The key bound to [`edit:apply-autofix`]() will be shown when an # [autofix](#autofix) is available. var insert:binding # A boolean used to control whether text pasted using # [bracketed paste](https://en.wikipedia.org/wiki/Bracketed-paste) # in the terminal should be quoted as a string. Defaults to `$false`. var insert:quote-paste elvish-0.21.0/pkg/edit/insert_api.go000066400000000000000000000027731465720375400173040ustar00rootroot00000000000000package edit import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) func initInsertAPI(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { simpleAbbr := vals.EmptyMap simpleAbbrVar := vars.FromPtr(&simpleAbbr) appSpec.SimpleAbbreviations = makeMapIterator(simpleAbbrVar) commandAbbr := vals.EmptyMap commandAbbrVar := vars.FromPtr(&commandAbbr) appSpec.CommandAbbreviations = makeMapIterator(commandAbbrVar) 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", simpleAbbrVar) nb.AddVar("command-abbr", commandAbbrVar) 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().(vals.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.21.0/pkg/edit/insert_api_test.go000066400000000000000000000027661465720375400203450ustar00rootroot00000000000000package edit import ( "testing" "src.elv.sh/pkg/cli/term" ) func TestInsert_Abbr(t *testing.T) { f := setup(t) evals(f.Evaler, `set 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, `var called = 0`, `set edit:insert:binding[x] = { set 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, `set 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, `var v0 = $edit:insert:quote-paste`, `edit:toggle-quote-paste`, `var v1 = $edit:insert:quote-paste`, `edit:toggle-quote-paste`, `var 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.21.0/pkg/edit/instant.d.elv000066400000000000000000000007361465720375400172270ustar00rootroot00000000000000#doc:show-unstable # Binding for the instant mode. var -instant:binding #doc:show-unstable # 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. fn -instant:start { } elvish-0.21.0/pkg/edit/instant.go000066400000000000000000000021471465720375400166220ustar00rootroot00000000000000package 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" ) 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]any{ "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 } ctx, done := eval.ListenInterrupts() err = ev.Eval( parse.Source{Name: "[instant]", Code: code}, eval.EvalCfg{Ports: []*eval.Port{nil, outPort}, Interrupts: ctx}) done() 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(modes.ErrorText(err)) } } elvish-0.21.0/pkg/edit/instant_test.go000066400000000000000000000021671465720375400176630ustar00rootroot00000000000000package 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.21.0/pkg/edit/key_binding.go000066400000000000000000000044041465720375400174220ustar00rootroot00000000000000package 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/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.DefaultKey) { return m.GetKey(ui.DefaultKey) } } return nil } func callWithNotifyPorts(nt notifier, ev *eval.Evaler, f eval.Callable, args ...any) { 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 any) 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.ReprPlain(v)) } 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.21.0/pkg/edit/listing.d.elv000066400000000000000000000030441465720375400172130ustar00rootroot00000000000000# Common binding table for [listing modes](#listing-modes). var listing:binding { } # Accepts the current selected listing item. fn listing:accept { } # Moves the cursor up in listing mode. fn listing:up { } # Moves the cursor down in listing mode. fn listing:down { } # Moves the cursor up in listing mode, or to the last item if the first item is # currently selected. fn listing:up-cycle { } # Moves the cursor down in listing mode, or to the first item if the last item is # currently selected. fn listing:down-cycle { } # Moves the cursor up one page. fn listing:page-up { } # Moves the cursor down one page. fn listing:page-down { } # Starts the history listing mode. fn histlist:start { } # Toggles deduplication in history listing mode. # # When deduplication is on (the default), only the last occurrence of the same # command is shown. fn histlist:toggle-dedup { } # Keybinding for the history listing mode. # # Keys bound to [edit:histlist:toggle-dedup](#edit:histlist:toggle-dedup) # (Ctrl-D by default) will be shown in the history listing UI. var histlist:binding # Starts the last command mode. fn lastcmd:start { } # Keybinding for the last command mode. var lastcmd:binding # Starts the location mode. fn location:start # Keybinding for the location mode. var location:binding # A list of directories to hide in the location addon. var location:hidden # A list of directories to always show at the top of the list of the location # addon. var location:pinned # A map mapping types of workspaces to their patterns. var location:workspaces elvish-0.21.0/pkg/edit/listing.go000066400000000000000000000145651465720375400166220ustar00rootroot00000000000000package 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/store/storedefs" "src.elv.sh/pkg/ui" ) 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]any{ "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 any) { 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) ns := eval.BuildNsNamed("edit:histlist"). AddVar("binding", bindingVar). AddGoFns(map[string]any{ "start": func() { w, err := modes.NewHistlist(ed.app, modes.HistlistSpec{ Bindings: bindings, AllCmds: histStore.AllCmds, Dedup: func() bool { return dedup.Get().(bool) }, Filter: filterSpec, CodeAreaRPrompt: func() ui.Text { return bindingTips(ed.ns, "histlist:binding", bindingTip("dedup", "histlist:toggle-dedup")) }, }) startMode(ed.app, w, err) }, "toggle-dedup": func() { dedup.Set(!dedup.Get().(bool)) listingRefilter(ed.app) ed.app.Redraw() }, }).Ns() nb.AddNs("histlist", ns) } 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.AfterChdir = append(ev.AfterChdir, func(string) { wd, err := os.Getwd() if err != nil { // TODO(xiaq): Surface the error. return } if st != nil { st.AddDir(wd, 1) kind, root := workspaceIterator.Parse(wd) if kind != "" { st.AddDir(kind+wd[len(root):], 1) } } }) } func listingAccept(app cli.App) { if w, ok := activeComboBox(app); ok { w.ListBox().Accept() } } func listingUp(app cli.App) { listingSelect(app, tk.Prev) } func listingDown(app cli.App) { listingSelect(app, tk.Next) } func listingUpCycle(app cli.App) { listingSelect(app, tk.PrevWrap) } func listingDownCycle(app cli.App) { listingSelect(app, tk.NextWrap) } func listingPageUp(app cli.App) { listingSelect(app, tk.PrevPage) } func listingPageDown(app cli.App) { listingSelect(app, tk.NextPage) } func listingLeft(app cli.App) { listingSelect(app, tk.Left) } 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() } } func adaptToIterateString(variable vars.Var) func(func(string)) { return func(f func(s string)) { vals.Iterate(variable.Get(), func(v any) 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().(vals.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) { if d.st == nil { // A "no daemon" build won't have have a storedefs.Store object. // Fail gracefully rather than panic. return []storedefs.Dir{}, nil } 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(modes.ErrorText(err)) } } func activeComboBox(app cli.App) (tk.ComboBox, bool) { w, ok := app.ActiveWidget().(tk.ComboBox) return w, ok } elvish-0.21.0/pkg/edit/listing_custom.d.elv000066400000000000000000000030201465720375400205770ustar00rootroot00000000000000# Starts custom listing mode. # # The `$items` argument can be as a list of maps, each map representing one item # and having the following keys: # # - The value of the `to-show` key must be a string or a styled text. It is used # in the listing UI. # # - The value of the `to-filter` key must be a string. It is used when filtering # the item. # # - The value of the `to-accept` key must be a string. It is passed to the # accept callback (see below). # # Alternatively, the `$items` argument can be a function taking one argument. It # will be called with the value of the filter (initially an empty string), and # can output any number of maps containing the `to-show` and `to-accept` keys, # with the same semantics as above. Any other key is ignored. # # The `&binding` option, if specified, should be a binding map to use in the # custom listing mode. Bindings from [`$edit:listing:binding`]() are also used, # after this map if it is specified. # # The `&caption` option changes the caption of the mode. If empty, the caption # defaults to `' LISTING '`. # # The `&keep-bottom` option, if true, makes the last item to get selected # initially or when the filter changes. # # The `&accept` option specifies a function to call when an item is accepted. It # is passed the value of the `to-accept` key of the item. # # The `&auto-accept` option, if true, accepts an item automatically when there # is only one item being shown. fn listing:start-custom {|items &binding=$nil &caption='' &keep-bottom=$false &accept=$nil &auto-accept=$false| } elvish-0.21.0/pkg/edit/listing_custom.go000066400000000000000000000061441465720375400202060ustar00rootroot00000000000000package 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() {} func listingStartCustom(ed *Editor, fm *eval.Frame, opts customListingOpts, items any) { 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 any) { 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, []any{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 any) 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 any) (string, bool) { toFilterValue, _ := vals.Index(v, "to-filter") toFilter, toFilterOk := toFilterValue.(string) return toFilter, toFilterOk } func getListingItem(v any) (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.21.0/pkg/edit/listing_nonwindows_test.go000066400000000000000000000046211465720375400221360ustar00rootroot00000000000000//go: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, `set edit:location:pinned = [/opt]`, `set 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, `set 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, `set 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.21.0/pkg/edit/listing_test.go000066400000000000000000000146111465720375400176510ustar00rootroot00000000000000package 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, " Ctrl-D dedup\n", Styles, " ++++++ ", " 2 echo\n", " 3 ls\n", " 4 LS ", Styles, "++++++++++++++++++++++++++++++++++++++++++++++++++", ) evals(f.Evaler, `edit:histlist:toggle-dedup`) f.TestTTY(t, "~> \n", " HISTORY ", Styles, "********* ", term.DotHere, " Ctrl-D dedup\n", Styles, " ++++++ ", " 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, " Ctrl-D dedup\n", Styles, " ++++++ ", " 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, " Ctrl-D dedup\n", Styles, " ++++++ ", " 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, `var 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, `var 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, `var 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.21.0/pkg/edit/listing_windows_test.go000066400000000000000000000047421465720375400214270ustar00rootroot00000000000000package 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, `set edit:location:pinned = ['C:\opt']`, `set 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, `set 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, `set 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.21.0/pkg/edit/minibuf.go000066400000000000000000000024451465720375400165740ustar00rootroot00000000000000package 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]any{ "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(modes.ErrorText(err)) } } elvish-0.21.0/pkg/edit/minibuf_test.go000066400000000000000000000005361465720375400176320ustar00rootroot00000000000000package 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.21.0/pkg/edit/navigation.d.elv000066400000000000000000000017451465720375400177070ustar00rootroot00000000000000# Name of the currently selected file in navigation mode. $nil if not in # navigation mode. var navigation:selected-file # Keybinding for the navigation mode. # # Keys bound to # [edit:navigation:trigger-filter](#edit:navigation:trigger-filter) (Ctrl-F by # default) and # [edit:navigation:trigger-shown-hidden](#edit:navigation:trigger-shown-hidden) # (Ctrl-H by default) will be shown in the navigation mode UI. var navigation:binding # Start the navigation mode. fn navigation:start { } # Inserts the selected filename. fn navigation:insert-selected { } # Inserts the selected filename and closes the navigation addon. fn navigation:insert-selected-and-quit { } # Toggles the filtering status of the navigation addon. fn navigation:trigger-filter { } # Toggles whether the navigation addon should be showing hidden files. fn navigation:trigger-shown-hidden { } # A list of 3 integers, used for specifying the width ratio of the 3 columns in # navigation mode. var navigation:width-ratio elvish-0.21.0/pkg/edit/navigation.go000066400000000000000000000101131465720375400172710ustar00rootroot00000000000000package 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" "src.elv.sh/pkg/ui" ) 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)) }) } func navInsertSelectedAndQuit(app cli.App) { navInsertSelected(app) closeMode(app) } func convertNavWidthRatio(v any) [3]int { var ( numbers []int hasErr bool ) vals.Iterate(v, func(elem any) 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() any { if w, ok := activeNavigation(ed.app); ok { return w.SelectedName() } return nil }) app := ed.app // TODO: Rename to $edit:navigation:selected-file after deprecation nb.AddVar("selected-file", selectedFileVar) ns := eval.BuildNsNamed("edit:navigation"). AddVars(map[string]vars.Var{ "binding": bindingVar, "width-ratio": widthRatioVar, }). AddGoFns(map[string]any{ "start": func() { w, err := modes.NewNavigation(app, modes.NavigationSpec{ Bindings: bindings, Cursor: modes.NewOSNavigationCursor(ev.Chdir), WidthRatio: func() [3]int { return convertNavWidthRatio(widthRatioVar.Get()) }, Filter: filterSpec, CodeAreaRPrompt: func() ui.Text { return bindingTips(ed.ns, "navigation:binding", bindingTip("hidden", "navigation:trigger-shown-hidden"), bindingTip("filter", "navigation:trigger-filter")) }, }) if err != nil { app.Notify(modes.ErrorText(err)) } 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) }), // TODO: Rename to trigger-show-hidden after deprecation "trigger-shown-hidden": actOnNavigation(app, func(w modes.Navigation) { w.MutateShowHidden(neg) }), }).Ns() nb.AddNs("navigation", ns) } 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.21.0/pkg/edit/navigation_test.go000066400000000000000000000123271465720375400203410ustar00rootroot00000000000000package edit import ( "path/filepath" "testing" "src.elv.sh/pkg/cli/lscolors" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" "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 Ctrl-H hidden Ctrl-F filter\n", Styles, "************ ++++++ ++++++ ", " d a \n", Styles, "###### ++++++++++++++++++ ", " e ", Styles, " //////////////////", ) // Test $edit:selected-file. evals(f.Evaler, `var 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 Ctrl-H hidden Ctrl-F filter\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, `set @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 Ctrl-H hidden Ctrl-F filter\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 Ctrl-H hidden Ctrl-F filter\n", Styles, "************ ++++++ ++++++ ", " a \n", Styles, " ", " e ", Styles, "######", ) } func TestNavigation_UsesEvalerChdir(t *testing.T) { f := setupNav(t) afterChdirCalled := false f.Evaler.AfterChdir = append(f.Evaler.AfterChdir, func(dir string) { afterChdirCalled = true }) f.TTYCtrl.Inject(term.K('N', ui.Ctrl)) f.TTYCtrl.Inject(term.K(ui.Down)) // select directory "e" f.TTYCtrl.Inject(term.K(ui.Right)) // mode into "e" f.TTYCtrl.Inject(term.K('[', ui.Ctrl)) // quit navigation mode f.TestTTY(t, filepath.Join("~", "d", "e"), "> ", term.DotHere) if !afterChdirCalled { t.Errorf("afterChdir not called") } } 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) must.Chdir("d") return f } elvish-0.21.0/pkg/edit/prompt.d.elv000066400000000000000000000011241465720375400170600ustar00rootroot00000000000000# See [Prompts](#prompts). var prompt #doc:show-unstable # See [Prompt Eagerness](#prompt-eagerness). var -prompt-eagerness # See [Stale Prompt](#stale-prompt). var prompt-stale-threshold # See [Stale Prompt](#stale-prompt). var prompt-stale-transformer. # See [Prompts](#prompts). var rprompt #doc:show-unstable # See [Prompt Eagerness](#prompt-eagerness). var -rprompt-eagerness # See [Stale Prompt](#stale-prompt). var rprompt-stale-threshold # See [Stale Prompt](#stale-prompt). var rprompt-stale-transformer. # See [RPrompt Persistency](#rprompt-persistency). var rprompt-persistent elvish-0.21.0/pkg/edit/prompt.go000066400000000000000000000075661465720375400164750ustar00rootroot00000000000000package 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" ) 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 ...any) ui.Text { var ( result ui.Text resultMutex sync.Mutex ) add := func(v any) { 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 any) { 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.21.0/pkg/edit/prompt_test.go000066400000000000000000000104701465720375400175200ustar00rootroot00000000000000package 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(`set edit:prompt = { put '#'; num 13; styled '> ' red }`)) f.TestTTY(t, "#13> ", Styles, " !!", term.DotHere) } func TestPrompt_ByteOutput(t *testing.T) { f := setup(t, rc(`set edit:prompt = { print 'bytes> ' }`)) f.TestTTY(t, "bytes> ", term.DotHere) } func TestPrompt_ParsesSGRInByteOutput(t *testing.T) { f := setup(t, rc(`set edit:prompt = { print "\033[31mred\033[m> " }`)) f.TestTTY(t, "red> ", Styles, "!!! ", term.DotHere) } func TestPrompt_NotifiesInvalidValueOutput(t *testing.T) { f := setup(t, rc(`set 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(`set edit:prompt = { fail ERROR }`)) f.TestTTYNotes(t, "[prompt error] ERROR\n", `see stack trace with "show $edit:exceptions[0]"`) evals(f.Evaler, `var excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } func TestRPrompt(t *testing.T) { f := setup(t, rc(`set edit:rprompt = { put 'RRR' }`)) f.TestTTY(t, "~> ", term.DotHere, strings.Repeat(" ", clitest.FakeTTYWidth-6)+"RRR") } func TestPromptEagerness(t *testing.T) { f := setup(t, rc( `var i = 0`, `set edit:prompt = { set i = (+ $i 1); put $i'> ' }`, `set 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( `var pipe = (file:pipe)`, `set edit:prompt = { nop (slurp < $pipe); put '> ' }`, `set 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( `var pipe = (file:pipe)`, `set edit:prompt = { nop (slurp < $pipe); put '> ' }`, `set edit:prompt-stale-threshold = `+scaledMsAsSec(50), `set 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( `var pipe = (file:pipe)`, `set edit:prompt = { nop (slurp < $pipe); put '> ' }`, `set edit:prompt-stale-threshold = `+scaledMsAsSec(50), `set 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, `var excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } func TestRPromptPersistent_True(t *testing.T) { testRPromptPersistent(t, `set edit:rprompt-persistent = $true`, "~> "+strings.Repeat(" ", clitest.FakeTTYWidth-6)+"RRR", "\n", term.DotHere, ) } func TestRPromptPersistent_False(t *testing.T) { testRPromptPersistent(t, `set edit:rprompt-persistent = $false`, "~> ", // no rprompt "\n", term.DotHere, ) } func testRPromptPersistent(t *testing.T, code string, finalBuf ...any) { f := setup(t, rc(`set 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.21.0/pkg/edit/repl.d.elv000066400000000000000000000022031465720375400165000ustar00rootroot00000000000000# 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`]() 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). # # See also [`$edit:command-duration`](). var after-command # 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 [`rc.elv`](command.html#rc-file) before printing the first prompt. # # See also [`$edit:after-command`](). var command-duration elvish-0.21.0/pkg/edit/repl.go000066400000000000000000000016101465720375400160760ustar00rootroot00000000000000package 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" ) 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) eval.CallHook(ev, nil, "$:after-command", afterCommandHook.Get().(vals.List), m) }) } elvish-0.21.0/pkg/edit/state_api.d.elv000066400000000000000000000010731465720375400175130ustar00rootroot00000000000000# Inserts the given text at the dot, moving the dot after the newly # inserted text. fn insert-at-dot {|text| } # Equivalent to assigning `$text` to `$edit:current-command`. fn replace-input {|text| } #doc:show-unstable # Contains the current position of the cursor, as a byte position within # `$edit:current-command`. var -dot # 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. var current-command elvish-0.21.0/pkg/edit/state_api.go000066400000000000000000000033311465720375400171070ustar00rootroot00000000000000package edit import ( "errors" "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" ) var errDotOutOfBoundary = errors.New("dot out of command boundary") func insertAtDot(app cli.App, text string) { codeArea, ok := focusedCodeArea(app) if !ok { return } codeArea.MutateState(func(s *tk.CodeAreaState) { s.Buffer.InsertAtDot(text) }) } 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)} }) } 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]any{ "insert-at-dot": func(s string) { insertAtDot(app, s) }, "replace-input": func(s string) { replaceInput(app, s) }, }) setDot := func(v any) error { var dot int err := vals.ScanToGo(v, &dot) if err != nil { return err } codeArea.MutateState(func(s *tk.CodeAreaState) { if dot < 0 || dot > len(s.Buffer.Content) { err = errDotOutOfBoundary } else { s.Buffer.Dot = dot } }) return err } getDot := func() any { return vals.FromGo(codeArea.CopyState().Buffer.Dot) } nb.AddVar("-dot", vars.FromSetGet(setDot, getDot)) setCurrentCommand := func(v any) error { var content string err := vals.ScanToGo(v, &content) if err != nil { return err } replaceInput(app, content) return nil } getCurrentCommand := func() any { return vals.FromGo(codeArea.CopyState().Buffer.Content) } nb.AddVar("current-command", vars.FromSetGet(setCurrentCommand, getCurrentCommand)) } elvish-0.21.0/pkg/edit/state_api_test.go000066400000000000000000000024701465720375400201510ustar00rootroot00000000000000package 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, `set edit:-dot = 0`) testCodeBuffer(t, f.Editor, tk.CodeBuffer{Content: "code", Dot: 0}) } func TestDotOutOfBoundary(t *testing.T) { f := setup(t) f.SetCodeBuffer(tk.CodeBuffer{Content: "", Dot: 0}) evals(f.Evaler, "var err = ?(set edit:-dot = 10)[reason]") testGlobal(t, f.Evaler, "err", errDotOutOfBoundary) } func TestCurrentCommand(t *testing.T) { f := setup(t) evals(f.Evaler, `set 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.21.0/pkg/edit/store_api.d.elv000066400000000000000000000016301465720375400175260ustar00rootroot00000000000000# 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 # ``` fn command-history {|&cmd-only=$false &dedup=$false &newest-first| } # Inserts the last word of the last command. fn insert-last-word { } elvish-0.21.0/pkg/edit/store_api.go000066400000000000000000000046401465720375400171270ustar00rootroot00000000000000package 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") 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] } } 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]any{ "command-history": func(fm *eval.Frame, opts cmdhistOpt) error { return commandHistory(opts, fuser, fm.ValueOutput()) }, "insert-last-word": func() { insertLastWord(app, fuser) }, }) } elvish-0.21.0/pkg/edit/store_api_test.go000066400000000000000000000042561465720375400201710ustar00rootroot00000000000000package 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, `var @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, `var @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, `var @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, `var @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, `var @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.21.0/pkg/edit/testutils_test.go000066400000000000000000000073221465720375400202410ustar00rootroot00000000000000package 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" "src.elv.sh/pkg/tt" ) // Aliases. var ( Args = tt.Args 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 any) func(*fixture) { return func(f *fixture) { f.Evaler.ExtendGlobal(eval.BuildNs().AddVar("temp", vars.NewReadOnly(val))) evals(f.Evaler, "set "+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. "set edit:prompt = { tilde-abbr $pwd; put '> ' }", // This will simplify most tests against the terminal. "set 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 ...any) *term.Buffer { return term.NewBufferBuilder(f.width).MarkLines(args...).Buffer() } func (f *fixture) TestTTY(t *testing.T, args ...any) { t.Helper() f.TTYCtrl.TestBuffer(t, f.MakeBuffer(args...)) } func (f *fixture) TestTTYNotes(t *testing.T, args ...any) { 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) any { v, _ := ev.Global().Index(name) return v } func testGlobals(t *testing.T, ev *eval.Evaler, wantVals map[string]any) { t.Helper() for name, wantVal := range wantVals { testGlobal(t, ev, name, wantVal) } } func testGlobal(t *testing.T, ev *eval.Evaler, name string, wantVal any) { t.Helper() if val := getGlobal(ev, name); !vals.Equal(val, wantVal) { t.Errorf("$%s = %s, want %s", name, vals.ReprPlain(val), vals.ReprPlain(wantVal)) } } 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.21.0/pkg/edit/transcripts_test.go000066400000000000000000000020331465720375400205470ustar00rootroot00000000000000package edit import ( "embed" "testing" "src.elv.sh/pkg/edit/complete" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { fnInGlobal := func(name string, impl any) func(*eval.Evaler) { return func(ev *eval.Evaler) { ev.ExtendBuiltin(eval.BuildNs().AddGoFn(name, impl)) } } evaltest.TestTranscriptsInFS(t, transcripts, "binding-map-in-global", fnInGlobal("binding-map", makeBindingMap), "wordify-in-global", fnInGlobal("wordify", wordify), "complete-getopt-in-global", fnInGlobal("complete-getopt", completeGetopt), "complete-filename-in-global", fnInGlobal("complete-filename", wrapArgGenerator(complete.GenerateFileNames)), "complex-candidate-in-global", fnInGlobal("complex-candidate", complexCandidate), "add-var-in-global", fnInGlobal("add-var", addVar), "add-vars-in-global", fnInGlobal("add-vars", addVars), "del-var-in-global", fnInGlobal("del-var", delVar), "del-vars-in-global", fnInGlobal("del-vars", delVars), ) } elvish-0.21.0/pkg/edit/vars.d.elv000066400000000000000000000061301465720375400165140ustar00rootroot00000000000000# Defines a new variable in the interactive REPL with an initial value. The new variable becomes # available during the next REPL cycle. # # Equivalent to running `var $name = $init` at a REPL prompt, but `$name` can be # dynamic. # # This is most useful for modules to modify the REPL namespace. Example: # # ```elvish-transcript # ~> cat .config/elvish/lib/a.elv # for i [(range 10)] { # edit:add-var foo$i $i # } # ~> use a # ~> put $foo1 $foo2 # ▶ (num 1) # ▶ (num 2) # ``` # # Note that if you use a variable as the `$init` argument, `edit:add-var` # doesn't add the variable "itself" to the REPL namespace. The variable in the # REPL namespace will have the initial value set to the variable's value, but # it is not an alias of the original variable: # # ```elvish-transcript # ~> cat .config/elvish/lib/b.elv # var foo = foo # edit:add-var foo $foo # ~> use b # ~> put $foo # ▶ foo # ~> set foo = bar # ~> echo $b:foo # foo # ``` # # ### Importing definition from a module into the REPL # # One common use of this command is to put the definitions of functions intended for REPL use in a # module instead of your [`rc.elv`](command.html#rc-file). For example, if you want to define `ll` # as `ls -l`, you can do so in your `rc.elv` directly: # # ```elvish # fn ll {|@a| ls -l $@a } # ``` # # But if you move the definition into a module (say `util.elv` in one of the # [module search directories](command.html#module-search-directories), this # function can only be used as `util:ll` (after `use util`). To make it usable # directly as `ll`, you can add the following to `util.elv`: # # ```elvish # edit:add-var ll~ $ll~ # ``` # # ### Conditionally importing a module # # Another use case is to add a module or function to the REPL namespace # conditionally. For example, to only import [the `unix` module](unix.html) # when actually running on Unix, a straightforward solution is to do the # following in `rc.elv`: # # ```elvish # use platform # if $platform:is-unix { # use unix # } # ``` # # This doesn't work however, since what `use` does is introducing a variable # named `$unix:`. Since all variables in Elvish are lexically scoped, the # `$unix:` variable is only valid inside the `if` block. # # This can be fixed by explicitly introducing the `$unix:` variable to the REPL # namespace. The following works both from `rc.elv` and from a module: # # ```elvish # use platform # if $platform:is-unix { # use unix # edit:add-var unix: $unix: # } # ``` fn add-var {|name init| } # Takes a map from strings to arbitrary values. Equivalent to calling # `edit:add-var` for each key-value pair in the map, but guarantees that all the # names will be added at the same time. fn add-vars {|map| } # Deletes a variable from the interactive REPL if it exists. # # Equivalent to running `del $name` at a REPL prompt, but `$name` can be # dynamic, and it is not an error to delete a non-existing variable. fn del-var {|name| } # Deletes variables from the interactive REPL. # # Equivalent to calling `edit:del-var` for each element of the list, but # guarantees that all the variables will be deleted at the same time. fn del-vars {|list| } elvish-0.21.0/pkg/edit/vars.go000066400000000000000000000043121465720375400161110ustar00rootroot00000000000000package edit import ( "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) func initVarsAPI(nb eval.NsBuilder) { nb.AddGoFns(map[string]any{ "add-var": addVar, "add-vars": addVars, "del-var": delVar, "del-vars": delVars, }) } func addVar(fm *eval.Frame, name string, val any) error { if !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, variable)) return nil } func delVar(fm *eval.Frame, name string) error { if !isUnqualified(name) { return errs.BadValue{ What: "name argument to edit:del-var", Valid: "unqualified variable name", Actual: name} } fm.Evaler.DeleteFromGlobal(map[string]struct{}{name: {}}) return nil } 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 !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 } func delVars(fm *eval.Frame, m vals.List) error { names := make(map[string]struct{}, m.Len()) for it := m.Iterator(); it.HasElem(); it.Next() { n := it.Elem() name, ok := n.(string) if !ok { return errs.BadValue{ What: "element of argument to edit:del-vars", Valid: "string", Actual: vals.Kind(n)} } if !isUnqualified(name) { return errs.BadValue{ What: "element of argument to edit:del-vars", Valid: "unqualified variable name", Actual: name} } names[name] = struct{}{} } fm.Evaler.DeleteFromGlobal(names) return nil } func isUnqualified(name string) bool { i := strings.IndexByte(name, ':') return i == -1 || i == len(name)-1 } elvish-0.21.0/pkg/edit/vars_test.elvts000066400000000000000000000040331465720375400177000ustar00rootroot00000000000000/////////// # add-var # /////////// //each:add-var-in-global ~> add-var foo bar ~> put $foo ▶ bar ## name must be unqualified ## ~> add-var a:b '' Exception: bad value: name argument to edit:add-var must be unqualified variable name, but is a:b [tty]:1:1-14: add-var a:b '' ## bad type ## ~> add-var a~ '' Exception: wrong type: need !!eval.Callable, got string [tty]:1:1-13: add-var a~ '' /////////// # del-var # /////////// //each:del-var-in-global ~> var foo = bar ~> del-var foo ~> put $foo Compilation error: variable $foo not found [tty]:1:5-8: put $foo ## deleting a non-existent variable is not an error ## ~> del-var foo ## name must be unqualified ## ~> del-var a:b Exception: bad value: name argument to edit:del-var must be unqualified variable name, but is a:b [tty]:1:1-11: del-var a:b //////////// # add-vars # //////////// //each:add-vars-in-global ~> add-vars [&foo=bar] ~> put $foo ▶ bar ~> add-vars [&a=A &b=B] ~> put $a $b ▶ A ▶ B ## key must be string ## ~> add-vars [&[]=''] Exception: bad value: key of argument to edit:add-vars must be string, but is list [tty]:1:1-17: add-vars [&[]=''] ## name must be unqualified ## ~> add-vars [&a:b=''] Exception: bad value: key of argument to edit:add-vars must be unqualified variable name, but is a:b [tty]:1:1-18: add-vars [&a:b=''] ## bad type ## ~> add-vars [&a~=''] Exception: wrong type: need !!eval.Callable, got string [tty]:1:1-17: add-vars [&a~=''] /////////// # del-var # /////////// //each:del-vars-in-global ## ? ## ~> var a b c ~> del-vars [a b] ~> put $a Compilation error: variable $a not found [tty]:1:5-6: put $a ~> put $b Compilation error: variable $b not found [tty]:1:5-6: put $b ~> put $c ▶ $nil ## key must be string ## ~> del-vars [[]] Exception: bad value: element of argument to edit:del-vars must be string, but is list [tty]:1:1-13: del-vars [[]] ## name must be unqualified ## ~> del-vars [a:b] Exception: bad value: element of argument to edit:del-vars must be unqualified variable name, but is a:b [tty]:1:1-14: del-vars [a:b] elvish-0.21.0/pkg/elvdoc/000077500000000000000000000000001465720375400151365ustar00rootroot00000000000000elvish-0.21.0/pkg/elvdoc/elvdoc.go000066400000000000000000000207151465720375400167460ustar00rootroot00000000000000// Package elvdoc extracts doc comments of Elvish variables and functions. // // An elvdoc is a continuous sequence of comment lines that consist of: // // 1. An optional sequence of "directive" lines, which start with "#" followed // immediately by a non-space character. // // 2. Any number of content lines, which are either a lone "#", or starts with // "# ". // // Elvdocs can appear before "var" or "fn" declarations, or at the top of the // module. // // There is one directive recognized by this package, "#doc:show-unstable". // Normally, symbols starting with "-" are ignored by this package, but adding // this directive suppresses that behavior. All other directives are left // unprocessed and returned. // // This package doesn't require the content lines to follow any syntax, but // other packages like pkg/mods/doc and the website generator assume them to be // Markdown. package elvdoc import ( "bufio" "fmt" "io" "io/fs" "regexp" "strings" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/strutil" ) // Docs records doc comments. type Docs struct { File *FileEntry Fns []Entry Vars []Entry } // FileEntry stores the file-level elvdoc. type FileEntry struct { Directives []string Content string LineNo int } // Entry stores the elvdoc for a particular symbol. type Entry struct { Name string Directives []string Content string LineNo int // 1-based line number for the first line of Content. 0 if Content is empty. Fn *Fn // Function-specific information, nil for variables. } // Returns e.Content, prepended with function usage if applicable. func (e Entry) FullContent() string { if e.Fn == nil { return e.Content } return fmt.Sprintf("```elvish\n%s\n```\n\n%s", e.Fn.Usage, e.Content) } // Fn stores fn-specific information. type Fn struct { // The signature without surrounding pipes, like "a @b". Signature string // Usage information converted from the original declaration. The function // name is qualified, like "ns:f $a $b...". Usage string } // ExtractAllFromFS extracts elvdocs of all modules found under fsys, and returns a map // from the symbol prefix of a module ("" for builtin, "$mod:" for any other $mod). // // See [ExtractFromFS] for how modules correspond to files. func ExtractAllFromFS(fsys fs.FS) (map[string]Docs, error) { prefixes := []string{"", "edit:"} modDirs, _ := fs.ReadDir(fsys, "mods") for _, modDir := range modDirs { if modDir.IsDir() { prefixes = append(prefixes, modDir.Name()+":") } } prefixToDocs := map[string]Docs{} for _, prefix := range prefixes { var err error prefixToDocs[prefix], err = ExtractFromFS(fsys, prefix) if err != nil { return nil, fmt.Errorf("extracting for prefix %q: %w", prefix, err) } } return prefixToDocs, nil } // ExtractFromFS extracts elvdoc of a module from fsys. The symbolPrefix is used // to look up which files to read: // // - "": eval/*.elv (the builtin module) // - "edit:": edit/*.elv // - "$mod:": mods/$mod/*.elv // // If symbolPrefix is not empty and doesn't end in ":", this function panics. func ExtractFromFS(fsys fs.FS, symbolPrefix string) (Docs, error) { var subdir string switch symbolPrefix { case "": subdir = "eval" case "edit:": subdir = "edit" default: if !strings.HasSuffix(symbolPrefix, ":") { panic("symbolPrefix must be empty or ends in :") } subdir = "mods/" + symbolPrefix[:len(symbolPrefix)-1] } filenames, err := fs.Glob(fsys, subdir+"/*.elv") if err != nil { return Docs{}, err } // Prepare to concatenate subDir/*.elv into one [io.Reader] to pass to // [Extract]. var readers []io.Reader for _, filename := range filenames { file, err := fsys.Open(filename) if err != nil { return Docs{}, err } // Insert an empty line between adjacent files so that the comment // block at the end of one file doesn't get merged with the comment // block at the start of the next file. readers = append(readers, file, strings.NewReader("\n\n")) } return Extract(io.MultiReader(readers...), symbolPrefix) } const ( singleQuoted = `'(?:[^']|'')*'` doubleQuoted = `"(?:[^\\"]|\\.)*"` // Bareword, single-quoted and double-quoted. The bareword pattern covers // more than what's allowed in Elvish syntax, but that's OK as the context // we'll use it in requires a string literal. stringLiteralGroup = `([^ '"]+|` + singleQuoted + `|` + doubleQuoted + `)` // Any run of non-pipe non-quote runes, or quoted strings. Again this covers // a superset of what's allowed, but that's OK. signatureGroup = `((?:[^|'"]|` + singleQuoted + `|` + doubleQuoted + `)*)` ) var ( // Groups: // 1. Name // 2. Signature (part inside ||) // // TODO: Support multi-line function signatures. fnRegexp = regexp.MustCompile(`^fn +` + stringLiteralGroup + ` +\{(?: *\|` + signatureGroup + `\|)?`) // Groups: // 1. Name varRegexp = regexp.MustCompile(`^var +` + stringLiteralGroup) ) const showUnstable = "#doc:show-unstable" // Keeps the state of the current elvdoc block. // // An elvdoc block contains a number of consecutive comment lines, followed // optionally by directive lines (#doc:html-id or #doc:show-unstable), and ends // with a fn/var/#doc:fn line. type blockState struct { directives []string content []string startLineNo int showUnstable bool } // Uses the state to set relevant fields in the Entry, and resets the state. func (b *blockState) finish() (directives []string, content string, lineNo int, showUnstable bool) { directives, content, lineNo, showUnstable = b.directives, strutil.JoinLines(b.content), b.startLineNo, b.showUnstable *b = blockState{} return } // Extract extracts the elvdoc of one module from an Elvish source. func Extract(r io.Reader, symbolPrefix string) (Docs, error) { var docs Docs var block blockState scanner := bufio.NewScanner(r) lineNo := 0 maybeSetFileEntry := func() { // This is a somewhat simplistic criteria for "top of the file", but // it's good enough for now. if len(docs.Vars) == 0 && len(docs.Fns) == 0 { if len(block.directives) > 0 || len(block.content) > 0 { directives, content, lineNo, _ := block.finish() docs.File = &FileEntry{directives, content, lineNo} } } } for scanner.Scan() { line := scanner.Text() lineNo++ if line == "#" || strings.HasPrefix(line, "# ") { if len(block.content) == 0 { block.startLineNo = lineNo } if line == "#" { block.content = append(block.content, "") } else { block.content = append(block.content, line[2:]) } } else if strings.HasPrefix(line, "#") { if len(block.content) > 0 { return Docs{}, fmt.Errorf("line %d: directive must appear at top of elvdoc block", lineNo) } if line == showUnstable { block.showUnstable = true } else { block.directives = append(block.directives, line[1:]) } } else if m := fnRegexp.FindStringSubmatch(line); m != nil { name, sig := unquote(m[1]), m[2] qname := symbolPrefix + name usage := fnUsage(qname, sig) directives, content, lineNo, showUnstable := block.finish() if showUnstable || !unstable(name) { docs.Fns = append(docs.Fns, Entry{qname, directives, content, lineNo, &Fn{sig, usage}}) } } else if m := varRegexp.FindStringSubmatch(line); m != nil { name := unquote(m[1]) directives, content, lineNo, showUnstable := block.finish() if showUnstable || !unstable(name) { docs.Vars = append(docs.Vars, Entry{"$" + symbolPrefix + name, directives, content, lineNo, nil}) } } else { maybeSetFileEntry() block = blockState{} } } maybeSetFileEntry() return docs, scanner.Err() } func unquote(s string) string { pn := &parse.Primary{} // TODO: Handle error parse.ParseAs(parse.Source{Code: s}, pn, parse.Config{}) return pn.Value } func fnUsage(name, sig string) string { var sb strings.Builder sb.WriteString(parse.QuoteCommandName(name)) for _, field := range sigFields(sig) { sb.WriteByte(' ') if strings.HasPrefix(field, "&") { sb.WriteString(field) } else if strings.HasPrefix(field, "@") { sb.WriteString("$" + field[1:] + "...") } else { sb.WriteString("$" + field) } } return sb.String() } func sigFields(sig string) []string { pn := &parse.Primary{} // TODO: Handle error parse.ParseAs(parse.Source{Code: "{|" + sig + "|}"}, pn, parse.Config{}) var fields []string for _, n := range parse.Children(pn) { if _, isSep := n.(*parse.Sep); isSep { continue } s := strings.TrimSpace(parse.SourceText(n)) if s != "" { fields = append(fields, s) } } return fields } func unstable(s string) bool { return s != "-" && strings.HasPrefix(s, "-") } elvish-0.21.0/pkg/elvdoc/elvdoc_test.go000066400000000000000000000117271465720375400200100ustar00rootroot00000000000000package elvdoc import ( "strings" "testing" "github.com/google/go-cmp/cmp" "src.elv.sh/pkg/testutil" ) var dedent = testutil.Dedent var extractTests = []struct { name string text string prefix string wantFile *FileEntry wantFns []Entry wantVars []Entry }{ { name: "fn with doc comment block", text: dedent(` # Adds numbers. fn add {|a b| } `), wantFns: []Entry{{ Name: "add", Content: "Adds numbers.\n", LineNo: 1, Fn: &Fn{Signature: "a b", Usage: "add $a $b"}, }}, }, { name: "fn with no doc comment", text: dedent(` fn add {|a b| } `), wantFns: []Entry{{ Name: "add", Fn: &Fn{Signature: "a b", Usage: "add $a $b"}, }}, }, { name: "fn with options in signature", text: dedent(` fn add {|a b &k=v| } `), wantFns: []Entry{{ Name: "add", Fn: &Fn{Signature: "a b &k=v", Usage: "add $a $b &k=v"}, }}, }, { name: "fn with single-quoted name", text: `fn 'all''s well' { }`, wantFns: []Entry{{ Name: "all's well", Fn: &Fn{Usage: "'all''s well'"}, }}, }, { name: "fn with double-quoted name", text: `fn "\\\"" { }`, wantFns: []Entry{{ Name: `\"`, Fn: &Fn{Usage: `'\"'`}, }}, }, { name: "fn with quoted string in option value", text: `fn add {|&a='| ' &b="\" "| }`, wantFns: []Entry{{ Name: "add", Fn: &Fn{Signature: `&a='| ' &b="\" "`, Usage: `add &a='| ' &b="\" "`}, }}, }, { name: "fn with rest argument in signature", text: `fn add {|a b @more| }`, wantFns: []Entry{{ Name: "add", Fn: &Fn{Signature: "a b @more", Usage: "add $a $b $more..."}, }}, }, { name: "var with doc comment block", text: dedent(` # Foo. var foo `), wantVars: []Entry{{ Name: "$foo", Content: "Foo.\n", LineNo: 1, }}, }, { name: "var with no doc comment", text: dedent(` var foo `), wantVars: []Entry{{ Name: "$foo", Content: "", }}, }, { name: "prefix impacts both fn and var", text: dedent(` var v fn f { } `), prefix: "foo:", wantVars: []Entry{{Name: "$foo:v"}}, wantFns: []Entry{{Name: "foo:f", Fn: &Fn{Usage: "foo:f"}}}, }, { name: "directive", text: dedent(` #foo # Adds numbers. fn add {|a b| } `), wantFns: []Entry{{ Name: "add", Directives: []string{"foo"}, Content: "Adds numbers.\n", LineNo: 2, Fn: &Fn{Signature: "a b", Usage: "add $a $b"}, }}, }, { name: "file-level comment block with no other block", text: dedent(` #foo # This is a module. # Foo. var foo `), wantFile: &FileEntry{[]string{"foo"}, "This is a module.\n", 2}, wantVars: []Entry{{ Name: "$foo", Content: "Foo.\n", LineNo: 4, }}, }, { name: "file-level comment block with no other block", text: dedent(` #foo # This is a module. `), wantFile: &FileEntry{[]string{"foo"}, "This is a module.\n", 2}, }, { name: "no file-level comment", text: dedent(` use a # Foo var foo `), wantVars: []Entry{{ Name: "$foo", Content: "Foo\n", LineNo: 3, }}, }, { name: "unstable symbol", text: dedent(` # Unstable. fn -foo { } `), wantFns: nil, }, { name: "unstable symbol with doc:show-unstable", text: dedent(` #doc:show-unstable # Unstable. fn -foo { } `), wantFns: []Entry{{ Name: "-foo", Content: "Unstable.\n", LineNo: 2, Fn: &Fn{Usage: "-foo"}, }}, }, { name: "empty line breaks comment block", text: dedent(` # Adds numbers. fn add {|a b| } `), wantFile: &FileEntry{Content: "Adds numbers.\n", LineNo: 1}, wantFns: []Entry{{ Name: "add", Fn: &Fn{Signature: "a b", Usage: "add $a $b"}, }}, }, { name: "empty comment line does not break comment block", text: dedent(` # Adds numbers. # # Supports two numbers. fn add {|a b| } `), wantFns: []Entry{{ Name: "add", Content: dedent(` Adds numbers. Supports two numbers. `), LineNo: 1, Fn: &Fn{Signature: "a b", Usage: "add $a $b"}, }}, }, { name: "line number tracking", text: dedent(` # Foo # function fn foo { } # Bar # function fn bar { } # Lorem # variable var lorem `), wantFns: []Entry{ {Name: "foo", Content: "Foo\nfunction\n", LineNo: 1, Fn: &Fn{Usage: "foo"}}, {Name: "bar", Content: "Bar\nfunction\n", LineNo: 5, Fn: &Fn{Usage: "bar"}}, }, wantVars: []Entry{ {Name: "$lorem", Content: "Lorem\nvariable\n", LineNo: 9}, }, }, } func TestExtract(t *testing.T) { for _, tc := range extractTests { t.Run(tc.name, func(t *testing.T) { docs, err := Extract(strings.NewReader(tc.text), tc.prefix) if err != nil { t.Errorf("error: %v", err) } if diff := cmp.Diff(tc.wantFile, docs.File); diff != "" { t.Errorf("unexpected File:\n%s", diff) } if diff := cmp.Diff(tc.wantFns, docs.Fns); diff != "" { t.Errorf("unexpected Fns:\n%s", diff) } if diff := cmp.Diff(tc.wantVars, docs.Vars); diff != "" { t.Errorf("unexpected Vars:\n%s", diff) } }) } } elvish-0.21.0/pkg/elvdoc/highlight.go000066400000000000000000000034501465720375400174360ustar00rootroot00000000000000package elvdoc import ( "regexp" "strings" "src.elv.sh/pkg/edit/highlight" "src.elv.sh/pkg/ui" ) // With an empty highlight.Config, this highlighter does not check for // compilation errors or non-existent commands. var highlighter = highlight.NewHighlighter(highlight.Config{}) // HighlightCodeBlock highlights a code block from Markdown. It handles thea // elvish and elvish-transcript languages. It also removes comment and directive // lines from elvish-transcript code blocks. func HighlightCodeBlock(info, code string) ui.Text { language, _, _ := strings.Cut(info, " ") switch language { case "elvish": t, _ := highlighter.Get(code) return t case "elvish-transcript": return highlightTranscript(code) default: return ui.T(code) } } // Pattern for the prefix of the first line of Elvish code in a transcript. var ps1Pattern = regexp.MustCompile(`^[~/][^ ]*> `) // TODO: Ideally this should use the parser in [src.elv.sh/pkg/transcript], func highlightTranscript(code string) ui.Text { var tb ui.TextBuilder lines := strings.Split(code, "\n") for i := 0; i < len(lines); i++ { line := lines[i] if ps1 := ps1Pattern.FindString(line); ps1 != "" { elvishLines := []string{line[len(ps1):]} // Include lines that are indented with the same length of ps1. ps2 := strings.Repeat(" ", len(ps1)) for i++; i < len(lines) && strings.HasPrefix(lines[i], ps2); i++ { elvishLines = append(elvishLines, lines[i]) } i-- highlighted, _ := highlighter.Get(strings.Join(elvishLines, "\n")) tb.WriteText(ui.T(ps1)) tb.WriteText(highlighted) } else if strings.HasPrefix(line, "//") { // Suppress comment/directive line. continue } else { // Write an output line. tb.WriteText(ui.T(line)) } if i < len(lines)-1 { tb.WriteText(ui.T("\n")) } } return tb.Text() } elvish-0.21.0/pkg/elvdoc/highlight_test.go000066400000000000000000000032421465720375400204740ustar00rootroot00000000000000package elvdoc_test import ( "reflect" "testing" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) var Dedent = testutil.Dedent var stylesheet = ui.RuneStylesheet{ 'v': ui.FgGreen, '$': ui.FgMagenta, } var highlightCodeBlockTests = []struct { name string info string code string want ui.Text }{ { name: "elvish", info: "elvish extra info", code: "echo $pwd", want: ui.MarkLines( "echo $pwd", stylesheet, "vvvv $$$$"), }, { name: "elvish-transcript", info: "elvish-transcript extra info", code: Dedent(` ~> echo $pwd /home/elf `), want: ui.MarkLines( "~> echo $pwd\n", stylesheet, " vvvv $$$$", "/home/elf\n", ), }, { name: "elvish-transcript multi line", info: "elvish-transcript extra info", code: Dedent(` ~> echo $pwd echo $pwd /home/elf /home/elf `), want: ui.MarkLines( "~> echo $pwd\n", stylesheet, " vvvv $$$$", " echo $pwd\n", stylesheet, " vvvv $$$$", "/home/elf\n", "/home/elf\n", ), }, { name: "elvish-transcript suppress comment/directive", info: "elvish-transcript extra info", code: Dedent(` //dir // A comment ~> echo $pwd /home/elf `), want: ui.MarkLines( "~> echo $pwd\n", stylesheet, " vvvv $$$$", "/home/elf\n", ), }, { name: "other languages", info: "bash", code: "echo $pwd", want: ui.T("echo $pwd"), }, } func TestHighlightCodeBlock(t *testing.T) { for _, tc := range highlightCodeBlockTests { t.Run(tc.name, func(t *testing.T) { got := elvdoc.HighlightCodeBlock(tc.info, tc.code) if !reflect.DeepEqual(got, tc.want) { t.Errorf("got %s, want %s", got, tc.want) } }) } } elvish-0.21.0/pkg/env/000077500000000000000000000000001465720375400144525ustar00rootroot00000000000000elvish-0.21.0/pkg/env/env.go000066400000000000000000000012171465720375400155720ustar00rootroot00000000000000// 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" NO_COLOR = "NO_COLOR" 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.21.0/pkg/errutil/000077500000000000000000000000001465720375400153505ustar00rootroot00000000000000elvish-0.21.0/pkg/errutil/errutil.go000066400000000000000000000001141465720375400173610ustar00rootroot00000000000000// Package errutil contains common error-related utilities. package errutil elvish-0.21.0/pkg/errutil/multi_error.go000066400000000000000000000021451465720375400202440ustar00rootroot00000000000000package errutil import "strings" // Multi combines 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 an error whose Error methods contain all // the messages of all non-nil arguments. // // If the input contains any error returned by Multi, such errors are flattened. // The following two calls return the same value: // // Multi(Multi(err1, err2), Multi(err3, err4)) // Multi(err1, err2, err3, err4) func Multi(errs ...error) error { var nonNil []error for _, err := range errs { if err != nil { if multi, ok := err.(multiError); ok { nonNil = append(nonNil, multi...) } else { nonNil = append(nonNil, err) } } } switch len(nonNil) { case 0: return nil case 1: return nonNil[0] default: return multiError(nonNil) } } type multiError []error func (me multiError) Error() string { var sb strings.Builder sb.WriteString("multiple errors: ") for i, e := range me { if i > 0 { sb.WriteString("; ") } sb.WriteString(e.Error()) } return sb.String() } elvish-0.21.0/pkg/errutil/multi_error_test.go000066400000000000000000000016551465720375400213100ustar00rootroot00000000000000package errutil 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 }{ {Multi(), ""}, {Multi(errors.New("some error")), "some error"}, { Multi(err1, err2), "multiple errors: error 1; error 2", }, { Multi(err1, err2, err3), "multiple errors: error 1; error 2; error 3", }, { Multi(err1, Multi(err2, err3)), "multiple errors: error 1; error 2; error 3", }, { Multi(Multi(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.21.0/pkg/eval/000077500000000000000000000000001465720375400146115ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/benchmarks_test.go000066400000000000000000000017121465720375400203150ustar00rootroot00000000000000package 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 := compile(ev.builtin.static(), ev.global.static(), nil, tree, 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.21.0/pkg/eval/builtin_fn_cmd.d.elv000066400000000000000000000023061465720375400205200ustar00rootroot00000000000000#//skip-test # Construct a callable value for the external program `$program`. Example: # # ```elvish-transcript # ~> var x = (external man) # ~> $x ls # opens the manpage for ls # ``` # # See also [`has-external`]() and [`search-external`](). fn external {|program| } # Test whether `$command` names a valid external command. Examples (your output # might differ): # # ```elvish-transcript # ~> has-external cat # ▶ $true # ~> has-external lalala # ▶ $false # ``` # # See also [`external`]() and [`search-external`](). fn has-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 # ``` # # See also [`external`]() and [`has-external`](). fn search-external {|command| } # 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". fn exec {|command? @args| } # Exit the Elvish process with `$status` (defaulting to 0). fn exit {|status?| } elvish-0.21.0/pkg/eval/builtin_fn_cmd.go000066400000000000000000000016551465720375400201230ustar00rootroot00000000000000package eval import ( "os" "os/exec" "src.elv.sh/pkg/eval/errs" ) // Command and process control. // TODO(xiaq): Document "fg". func init() { addBuiltinFns(map[string]any{ // Command resolution "external": external, "has-external": hasExternal, "search-external": searchExternal, // Process control "fg": fg, "exec": execFn, "exit": exit, }) } func external(cmd string) Callable { return NewExternalCmd(cmd) } func hasExternal(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } func searchExternal(cmd string) (string, error) { return exec.LookPath(cmd) } // Can be overridden in tests. var osExit = os.Exit 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)} } fm.Evaler.PreExit() osExit(code) return nil } elvish-0.21.0/pkg/eval/builtin_fn_cmd_test.elvts000066400000000000000000000014331465720375400217040ustar00rootroot00000000000000//////// # exit # //////// ## default code is 0 ## //check-exit-code-afterwards 0 ~> exit ## explicit code ## //check-exit-code-afterwards 1 ~> exit 1 ## runs pre-exit hooks ## //check-pre-exit-hook-afterwards ~> exit ## wrong arity ## ~> exit 1 2 Exception: arity mismatch: arguments must be 0 to 1 values, but is 2 values [tty]:1:1-8: exit 1 2 ///////////////////// # external commands # ///////////////////// //only-on unix //set-env PATH /bin ~> has-external sh ▶ $true ~> search-external sh ▶ /bin/sh ~> (external sh) -c 'echo external-sh' external-sh ~> has-external random-invalid-command ▶ $false ~> search-external random-invalid-command Exception: exec: "random-invalid-command": executable file not found in $PATH [tty]:1:1-38: search-external random-invalid-command elvish-0.21.0/pkg/eval/builtin_fn_cmd_unix.go000066400000000000000000000041661465720375400211660ustar00rootroot00000000000000//go:build unix 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") // Reference to syscall.Exec. Can be overridden in tests. var syscallExec = syscall.Exec func execFn(fm *Frame, args ...any) 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 } fm.Evaler.PreExit() 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.21.0/pkg/eval/builtin_fn_cmd_windows.go000066400000000000000000000003411465720375400216640ustar00rootroot00000000000000package eval import "errors" var errNotSupportedOnWindows = errors.New("not supported on Windows") func execFn(...any) error { return errNotSupportedOnWindows } func fg(...int) error { return errNotSupportedOnWindows } elvish-0.21.0/pkg/eval/builtin_fn_container.d.elv000066400000000000000000000071431465720375400217430ustar00rootroot00000000000000# Constructs a namespace from `$map`, using the keys as variable names and the # values as their values. Examples: # # ```elvish-transcript # ~> var n = (ns [&name=value]) # ~> put $n[name] # ▶ value # ~> var n: = (ns [&name=value]) # ~> put $n:name # ▶ value # ``` fn ns {|map| } # Outputs a map from the [value inputs](#value-inputs), each of which must be # an iterable value with with two elements. The first element of each value # 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] # ``` fn make-map {|input?| } # Outputs a list created from adding values in `$more` to the end of `$list`. # # The output is the same as `[$@list $more...]`, but the time complexity is # guaranteed to be O(m), where m is the number of values in `$more`. # # Examples: # # ```elvish-transcript # ~> conj [] a # ▶ [a] # ~> conj [a b] c d # ▶ [a b c d] # ``` # # Etymology: [Clojure](https://clojuredocs.org/clojure.core/conj). fn conj {|list @more| } # 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 # ▶ [&k=v &k2=v2] # ``` # # Etymology: [Clojure](https://clojuredocs.org/clojure.core/assoc). # # See also [`dissoc`](). fn assoc {|container k v| } # 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 # ▶ [&foo=bar &lorem=ipsum] # ``` # # See also [`assoc`](). fn dissoc {|map k| } # 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 # ``` fn has-value {|container value| } # 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 # ``` fn has-key {|container key| } #//skip-test # 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. fn keys {|map| } elvish-0.21.0/pkg/eval/builtin_fn_container.go000066400000000000000000000050721465720375400213370ustar00rootroot00000000000000package eval import ( "errors" "fmt" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) // Lists and maps. func init() { addBuiltinFns(map[string]any{ "ns": nsFn, "make-map": makeMap, "conj": conj, "assoc": assoc, "dissoc": dissoc, "has-key": hasKey, "has-value": hasValue, "keys": keys, }) } func nsFn(m vals.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 } func makeMap(input Inputs) (vals.Map, error) { m := vals.EmptyMap var errMakeMap error input(func(v any) { 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 } func conj(li vals.List, more ...any) vals.List { for _, val := range more { li = li.Conj(val) } return li } func assoc(a, k, v any) (any, error) { return vals.Assoc(a, k, v) } var errCannotDissoc = errors.New("cannot dissoc") func dissoc(a, k any) (any, error) { a2 := vals.Dissoc(a, k) if a2 == nil { return nil, errCannotDissoc } return a2, nil } func hasValue(container, value any) (bool, error) { switch container := container.(type) { case vals.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 any) bool { if vals.Equal(v, value) { found = true return false } return true }) return found, err } } func hasKey(container, key any) bool { return vals.HasKey(container, key) } func keys(fm *Frame, v any) error { out := fm.ValueOutput() var errPut error errIterate := vals.IterateKeys(v, func(k any) bool { errPut = out.Put(k) return errPut == nil }) if errIterate != nil { return errIterate } return errPut } elvish-0.21.0/pkg/eval/builtin_fn_container_test.elvts000066400000000000000000000050541465720375400231260ustar00rootroot00000000000000////// # ns # ////// // Accessing like a map ~> put (ns [&name=value])[name] ▶ value // Accessing using : ~> var n: = (ns [&name=value]) put $n:name ▶ value ## bad key in argument ## ~> ns [&[]=[]] Exception: bad value: key of argument of "ns" must be string, but is list [tty]:1:1-11: ns [&[]=[]] //////////// # make-map # //////////// ~> make-map [] ▶ [&] ~> make-map [[k v]] ▶ [&k=v] ~> make-map [[k1 v1] [k2 v2]] ▶ [&k1=v1 &k2=v2] // Later key override previous one ~> make-map [[k v] [k v2]] ▶ [&k=v2] // String of length 2 also works ~> make-map [kv] ▶ [&k=v] ## bad argument ## ~> make-map [{ } [k v]] Exception: bad value: input to make-map must be iterable, but is fn [tty]:1:1-20: make-map [{ } [k v]] ~> make-map [[k v] [k]] Exception: bad value: input to make-map must be iterable with 2 elements, but is list with 1 elements [tty]:1:1-20: make-map [[k v] [k]] //////// # conj # //////// ~> conj [] a ▶ [a] ~> conj [a b] ▶ [a b] ~> conj [a b] c ▶ [a b c] ~> conj [a b] c d ▶ [a b c d] ///////// # assoc # ///////// ~> assoc [old] 0 new ▶ [new] ~> assoc [&] k v ▶ [&k=v] ~> assoc [&k=old] k new ▶ [&k=new] ////////// # dissoc # ////////// ~> dissoc [&k=v] k ▶ [&] ## bad argument ## ~> dissoc foo 0 Exception: cannot dissoc [tty]:1:1-12: dissoc foo 0 /////////// # has-key # /////////// ~> has-key [&k=v] k ▶ $true ~> has-key [&k=v] bad ▶ $false ~> has-key [lorem ipsum] 0 ▶ $true ~> has-key [lorem ipsum] 2 ▶ $false ## list slices ## ~> has-key [lorem ipsum] 0.. ▶ $true ~> has-key [lorem ipsum] 0..= ▶ $true ~> has-key [lorem ipsum] ..2 ▶ $true ~> has-key [lorem ipsum] ..=2 ▶ $false ~> has-key [lorem ipsum dolor sit] 0..4 ▶ $true ~> has-key [lorem ipsum dolor sit] 0..=4 ▶ $false ~> has-key [lorem ipsum dolor sit] 1..3 ▶ $true ~> has-key [lorem ipsum dolor sit] 1..5 ▶ $false ~> has-key [lorem ipsum dolor sit] -2..=-1 ▶ $true ///////////// # has-value # ///////////// ~> has-value [&lorem=ipsum &foo=bar] lorem ▶ $false ~> has-value [&lorem=ipsum &foo=bar] bar ▶ $true ~> has-value [foo bar] bar ▶ $true ~> has-value [foo bar] badehose ▶ $false ~> has-value [[foo] [bar]] [foo] ▶ $true ~> has-value "foo" o ▶ $true ~> has-value "foo" d ▶ $false //////// # keys # //////// ~> keys [&] ~> keys [&a=foo] ▶ a ~> keys [&a=foo &b=bar] | order ▶ a ▶ b ## bad argument ## ~> keys (num 1) Exception: cannot iterate keys of number [tty]:1:1-12: keys (num 1) ## propagates output error ## ~> keys [&a=foo] >&- Exception: port does not support value output [tty]:1:1-17: keys [&a=foo] >&- elvish-0.21.0/pkg/eval/builtin_fn_debug.d.elv000066400000000000000000000021631465720375400210440ustar00rootroot00000000000000#//skip-test # Output a map describing the current source, which is the source file or # interactive command that contains the call to `src`. 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 # ~> src # ▶ [&code=src &is-file=$false &name='[tty 1]'] # ~> elvish show-src.elv # ▶ [&code="src\n" &is-file=$true &name=/home/elf/show-src.elv] # ~> echo src > .config/elvish/lib/show-src.elv # ~> use show-src # ▶ [&code="src\n" &is-file=$true &name=/home/elf/.config/elvish/lib/show-src.elv] # ``` fn src { } #doc:show-unstable # Force the Go garbage collector to run. # # This is only useful for debug purposes. fn -gc { } #doc:show-unstable # Print a stack trace. # # This is only useful for debug purposes. fn -stack { } #doc:show-unstable # Direct internal debug logs to the named file. # # This is only useful for debug purposes. fn -log {|filename| } elvish-0.21.0/pkg/eval/builtin_fn_debug.go000066400000000000000000000011171465720375400204370ustar00rootroot00000000000000package eval import ( "runtime" "src.elv.sh/pkg/logutil" "src.elv.sh/pkg/parse" ) func init() { addBuiltinFns(map[string]any{ "src": src, "-gc": _gc, "-stack": _stack, "-log": _log, }) } func src(fm *Frame) parse.Source { return fm.src } func _gc() { runtime.GC() } 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 } func _log(fname string) error { return logutil.SetOutputFile(fname) } elvish-0.21.0/pkg/eval/builtin_fn_env.d.elv000066400000000000000000000034031465720375400205440ustar00rootroot00000000000000# Sets an environment variable to the given value. Calling `set-env VAR_NAME # value` is similar to `set E:VAR_NAME = value`, but allows the variable name # to be dynamic. # # Example: # # ```elvish-transcript # //unset-env X # ~> set-env X foobar # ~> put $E:X # ▶ foobar # ``` # # See also [`get-env`](), [`has-env`](), and [`unset-env`](). fn set-env {|name value| } # Unset an environment variable. Calling `unset-env VAR_NAME` is similar to # `del E:VAR_NAME`, but allows the variable name to be dynamic. # # Example: # # ```elvish-transcript # //unset-env X # ~> set E:X = foo # ~> unset-env X # ~> has-env X # ▶ $false # ~> put $E:X # ▶ '' # ``` # # See also [`has-env`](), [`get-env`](), and [`set-env`](). fn unset-env {|name| } # Test whether an environment variable exists. This command has no equivalent # operation using the `E:` namespace (but see https://b.elv.sh/1026). # # Examples: # # ```elvish-transcript # //set-env PATH /bin # //unset-env NO_SUCH_ENV # ~> has-env PATH # ▶ $true # ~> has-env NO_SUCH_ENV # ▶ $false # ``` # # See also [`get-env`](), [`set-env`](), and [`unset-env`](). fn has-env {|name| } # Gets the value of an environment variable. Throws an exception if the # environment variable does not exist. # # Calling `get-env VAR_NAME` is similar to `put $E:VAR_NAME`, but allows the # variable name to be dynamic, and throws an exception instead of producing an # empty string for nonexistent environment variables. # # Examples: # # ```elvish-transcript # //set-env LANG zh_CN.UTF-8 # //unset-env NO_SUCH_ENV # ~> get-env LANG # ▶ zh_CN.UTF-8 # ~> get-env NO_SUCH_ENV # Exception: non-existent environment variable # [tty]:1:1-19: get-env NO_SUCH_ENV # ``` # # See also [`has-env`](), [`set-env`](), and [`unset-env`](). fn get-env {|name| } elvish-0.21.0/pkg/eval/builtin_fn_env.go000066400000000000000000000011171465720375400201410ustar00rootroot00000000000000package 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") func init() { addBuiltinFns(map[string]any{ "has-env": hasEnv, "get-env": getEnv, "set-env": os.Setenv, "unset-env": os.Unsetenv, }) } func hasEnv(key string) bool { _, ok := os.LookupEnv(key) return ok } func getEnv(key string) (string, error) { value, ok := os.LookupEnv(key) if !ok { return "", ErrNonExistentEnvVar } return value, nil } elvish-0.21.0/pkg/eval/builtin_fn_env_test.elvts000066400000000000000000000010531465720375400217270ustar00rootroot00000000000000/////////// # get-env # /////////// ## outputs value of existing env variable ## //set-env var test-val ~> get-env var ▶ test-val ~> put $E:var ▶ test-val ## throws if env variable doesn't exist ## //unset-env var ~> get-env var Exception: non-existent environment variable [tty]:1:1-11: get-env var /////////// # has-env # /////////// ## exists ## //set-env var test-val ~> has-env var ▶ $true ## doesn't exist ## //unset-env var ~> has-env var ▶ $false /////////// # set-env # /////////// ~> set-env var test-val ~> echo $E:var test-val elvish-0.21.0/pkg/eval/builtin_fn_flow.d.elv000066400000000000000000000160151465720375400207260ustar00rootroot00000000000000# 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 # ~> use file # ~> 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. # # See also [`peach`](). fn run-parallel {|@callable| } # Calls `$f` on each [value input](#value-inputs). # # An exception raised from [`break`]() is caught by `each`, and will cause it to # terminate early. # # An exception raised from [`continue`]() is swallowed and can be used to # terminate a single iteration early. # # Examples: # # ```elvish-transcript # ~> range 5 8 | each {|x| * $x $x } # ▶ (num 25) # ▶ (num 36) # ▶ (num 49) # ~> each {|x| put $x[..3] } [lorem ipsum] # ▶ lor # ▶ ips # ``` # # See also [`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). fn each {|f inputs?| } # Calls `$f` for each [value input](#value-inputs), possibly in parallel. # # Like `each`, an exception raised from [`break`]() will cause `peach` to # terminate early. However due to the parallel nature of `peach`, the exact time # of termination is non-deterministic, and termination is not guaranteed. # # An exception raised from [`continue`]() is swallowed and can be used to # terminate a single iteration early. # # The `&num-workers` option restricts the number of functions that may run in # parallel, and must be either an exact positive or `+inf`. A value of `+inf` # (the default) means no restriction. Note that `peach &num-workers=1` is # equivalent to `each`. # # Example (your output will differ): # # ```elvish-transcript # //skip-test # ~> 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`. # # See also [`each`]() and [`run-parallel`](). fn peach {|&num-workers=(num +inf) f inputs?| } # Throws an exception; `$v` may be any type. If `$v` is already an exception, # `fail` rethrows it. # # ```elvish-transcript # ~> fail bad # Exception: bad # [tty]:1:1-8: fail bad # ~> put ?(fail bad) # ▶ [^exception &reason=[^fail-error &content=bad &type=fail] &stack-trace=<...>] # ~> fn f { fail bad } # ~> fail ?(f) # Exception: bad # [tty]:1:8-16: fn f { fail bad } # [tty]:1:8-8: fail ?(f) # ``` fn fail {|v| } # 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 # ``` fn return { } # 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 # ``` fn break { } # 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 # ``` fn continue { } # Schedules a function to be called when execution reaches the end of the # current closure. The function is called with no arguments or options, and any # exception it throws gets propagated. # # Examples: # # ```elvish-transcript # ~> { defer { put foo }; put bar } # ▶ bar # ▶ foo # ~> defer { put foo } # Exception: defer must be called from within a closure # [tty]:1:1-17: defer { put foo } # ``` fn defer {|fn| } elvish-0.21.0/pkg/eval/builtin_fn_flow.go000066400000000000000000000105651465720375400203270ustar00rootroot00000000000000package eval import ( "errors" "math" "math/big" "sync" "sync/atomic" "golang.org/x/sync/semaphore" "src.elv.sh/pkg/errutil" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Flow control. // TODO(xiaq): Document "multi-error". func init() { addBuiltinFns(map[string]any{ "run-parallel": runParallel, // Exception and control "fail": fail, "multi-error": multiErrorFn, "return": returnFn, "break": breakFn, "continue": continueFn, "defer": deferFn, // Iterations. "each": each, "peach": 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(), function, &exceptions[i]) } wg.Wait() return MakePipelineError(exceptions) } func each(fm *Frame, f Callable, inputs Inputs) error { broken := false var err error inputs(func(v any) { if broken { return } newFm := fm.Fork() ex := f.Call(newFm, []any{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 } type peachOpt struct{ NumWorkers vals.Num } func (o *peachOpt) SetDefaultOptions() { o.NumWorkers = math.Inf(1) } func peach(fm *Frame, opts peachOpt, f Callable, inputs Inputs) error { var wg sync.WaitGroup var broken int32 var errMu sync.Mutex var err error var workerSema *semaphore.Weighted numWorkers, limited, err := parseNumWorkers(opts.NumWorkers) if err != nil { return err } if limited { workerSema = semaphore.NewWeighted(int64(numWorkers)) } ctx := fm.Context() inputs(func(v any) { if atomic.LoadInt32(&broken) != 0 { return } if workerSema != nil { workerSema.Acquire(ctx, 1) } wg.Add(1) go func() { newFm := fm.Fork() newFm.ports[0] = DummyInputPort ex := f.Call(newFm, []any{v}, NoOpts) newFm.Close() if ex != nil { switch Reason(ex) { case nil, Continue: // nop case Break: atomic.StoreInt32(&broken, 1) default: errMu.Lock() err = errutil.Multi(err, ex) defer errMu.Unlock() atomic.StoreInt32(&broken, 1) } } wg.Done() if workerSema != nil { workerSema.Release(1) } }() }) wg.Wait() return err } func parseNumWorkers(n vals.Num) (int, bool, error) { switch n := n.(type) { case int: if n >= 1 { return n, true, nil } case *big.Int: // A limit larger than MaxInt is equivalent to no limit. return 0, false, nil case float64: if math.IsInf(n, 1) { return 0, false, nil } } return 0, false, errs.BadValue{ What: "peach &num-workers", Valid: "exact positive integer or +inf", Actual: vals.ToString(n), } } // FailError is an error returned by the "fail" command. type FailError struct{ Content any } var _ vals.PseudoMap = FailError{} // Error returns the string representation of the cause. func (e FailError) Error() string { return vals.ToString(e.Content) } // Kind returns "fail-error". func (FailError) Kind() string { return "fail-error" } // 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() any { return f.e.Content } func fail(v any) 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} } func returnFn() error { return Return } func breakFn() error { return Break } func continueFn() error { return Continue } var errDeferNotInClosure = errors.New("defer must be called from within a closure") func deferFn(fm *Frame, fn Callable) error { if fm.defers == nil { return errDeferNotInClosure } deferTraceback := fm.traceback fm.addDefer(func(fm *Frame) Exception { err := fn.Call(fm, NoArgs, NoOpts) if exc, ok := err.(Exception); ok { return exc } return &exception{err, deferTraceback} }) return nil } elvish-0.21.0/pkg/eval/builtin_fn_flow_test.elvts000066400000000000000000000116251465720375400221140ustar00rootroot00000000000000//////////////// # run-parallel # //////////////// ~> run-parallel { put lorem } { put ipsum } | order ▶ ipsum ▶ lorem ~> run-parallel { } { fail foo } Exception: foo [tty]:1:20-28: run-parallel { } { fail foo } [tty]:1:1-29: run-parallel { } { fail foo } //////// # each # //////// ~> put 1 233 | each $put~ ▶ 1 ▶ 233 ~> echo "1\n233" | each $put~ ▶ 1 ▶ 233 ~> echo "1\r\n233" | each $put~ ▶ 1 ▶ 233 ~> each $put~ [1 233] ▶ 1 ▶ 233 ~> range 10 | each {|x| if (== $x 4) { break }; put $x } ▶ (num 0) ▶ (num 1) ▶ (num 2) ▶ (num 3) ~> range 10 | each {|x| if (== $x 4) { continue }; put $x } ▶ (num 0) ▶ (num 1) ▶ (num 2) ▶ (num 3) ▶ (num 5) ▶ (num 6) ▶ (num 7) ▶ (num 8) ▶ (num 9) ~> range 10 | each {|x| if (== $x 4) { fail haha }; put $x } ▶ (num 0) ▶ (num 1) ▶ (num 2) ▶ (num 3) Exception: haha [tty]:1:37-46: range 10 | each {|x| if (== $x 4) { fail haha }; put $x } [tty]:1:12-57: range 10 | each {|x| if (== $x 4) { fail haha }; put $x } // TODO: Test that "each" does not close the stdin. ///////// # peach # ///////// ~> range 5 | peach {|x| * 2 $x } | order ▶ (num 0) ▶ (num 2) ▶ (num 4) ▶ (num 6) ▶ (num 8) // continue ~> range 5 | peach {|x| if (== $x 2) { continue }; * 2 $x } | order ▶ (num 0) ▶ (num 2) ▶ (num 6) ▶ (num 8) ## processing order is non-deterministic ## // Test that inputs are not necessarily processed in order. // // 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. ~> 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 } } ▶ $true ## exception propagation ## ~> peach {|x| fail $x } [a] Exception: a [tty]:1:12-19: peach {|x| fail $x } [a] [tty]:1:1-24: peach {|x| fail $x } [a] ## break ## // It is technically possible for break to only take effect after the whole // sequence has been consumed, but that doesn't seem to ever happen in practice. ~> range 1 101 | peach {|x| if (== 50 $x) { break } else { put $x } } | < (+ (all)) (+ (range 1 101)) ▶ $true ## parallelism ## //test-time-scale-in-global // Test the parallelism of peach by observing its run time relative to the run // time of the function. Since the exact run time is subject to scheduling // differences, benchmark it multiple times and use the fastest run time. // Unlimited workers: when scheduling allows, no two function runs are serial, // so the best run time should be between t and 2t, regardless of input size. ~> var t = (* 0.005 $test-time-scale) var best-run = (benchmark &min-runs=5 &min-time=0 { range 6 | peach {|_| sleep $t } } &on-end={|metrics| put $metrics[min] }) < $t $best-run (* 2 $t) ▶ $true // 2 workers: when scheduling allows, at least two function runs are parallel. // On the other hand, No more than two functions are parallel. Best run time // should be between (ceil(n/2) * t) and n*t, where n is the input size. ~> var t = (* 0.005 $test-time-scale) var best-run = (benchmark &min-runs=5 &min-time=0 { range 6 | peach &num-workers=2 {|_| sleep $t } } &on-end={|metrics| put $metrics[min] }) < (* 3 $t) $best-run (* 6 $t) ▶ $true ## invalid &num-workers ## ~> peach &num-workers=0 {|x| * 2 $x } Exception: bad value: peach &num-workers must be exact positive integer or +inf, but is 0 [tty]:1:1-34: peach &num-workers=0 {|x| * 2 $x } ~> peach &num-workers=-2 {|x| * 2 $x } Exception: bad value: peach &num-workers must be exact positive integer or +inf, but is -2 [tty]:1:1-35: peach &num-workers=-2 {|x| * 2 $x } //////// # fail # //////// ~> fail haha Exception: haha [tty]:1:1-9: fail haha ~> fn f { fail haha } fail ?(f) Exception: haha [tty]:1:8-17: fn f { fail haha } [tty]:2:8-8: fail ?(f) ~> fail [] Exception: [] [tty]:1:1-7: fail [] ~> put ?(fail 1)[reason][type] ▶ fail ~> put ?(fail 1)[reason][content] ▶ 1 ////////// # return # ////////// ~> return Exception: return [tty]:1:1-6: return // Use of return inside fn is tested alongside fn in builtin_special_test.elvts. ///////// # defer # ///////// ~> { defer { put a }; put b } ▶ b ▶ a ~> { defer { put a }; fail bad } ▶ a Exception: bad [tty]:1:20-28: { defer { put a }; fail bad } [tty]:1:1-29: { defer { put a }; fail bad } ~> defer { } Exception: defer must be called from within a closure [tty]:1:1-9: defer { } ~> { defer { fail foo } } Exception: foo [tty]:1:11-19: { defer { fail foo } } [tty]:1:1-22: { defer { fail foo } } ~> { defer {|x| } } Exception: arity mismatch: arguments must be 1 value, but is 0 values [tty]:1:3-15: { defer {|x| } } [tty]:1:1-16: { defer {|x| } } elvish-0.21.0/pkg/eval/builtin_fn_fs.d.elv000066400000000000000000000014101465720375400203600ustar00rootroot00000000000000#//skip-test # Changes directory. # # This affects the entire process, including parallel tasks that are started # implicitly (such as prompt functions) or explicitly (such as one started by # [`peach`]()). # # Note that Elvish's `cd` does not support `cd -`. # # In interactive shells, [location mode](../learn/tour.html#directory-history) # provides an alternative to quickly change to past directories. # # See also [`$pwd`](). fn cd {|dirname| } # 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' # ``` fn tilde-abbr {|path| } elvish-0.21.0/pkg/eval/builtin_fn_fs.go000066400000000000000000000011541465720375400177620ustar00rootroot00000000000000package eval import ( "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/fsutil" ) // Filesystem commands. func init() { addBuiltinFns(map[string]any{ // Directory "cd": cd, // Path "tilde-abbr": tildeAbbr, }) } func cd(fm *Frame, args ...string) error { var dir string switch len(args) { case 0: var err error dir, err = 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) } func tildeAbbr(path string) string { return fsutil.TildeAbbr(path) } elvish-0.21.0/pkg/eval/builtin_fn_fs_test.elvts000066400000000000000000000011231465720375400215450ustar00rootroot00000000000000////////////// # tilde-abbr # ////////////// //with-temp-home ~> tilde-abbr ~/foobar ▶ '~/foobar' ////// # cd # ////// //each:with-temp-home //each:in-temp-dir ## explicit argument ## ~> use os use path os:mkdir ~/d1 ~> cd ~/d1 eq $pwd (path:join ~ d1) ▶ $true ## changes to home with no argument ## ~> cd ~> eq $pwd ~ ▶ $true ## arity check ## ~> cd dir1 dir2 Exception: arity mismatch: arguments must be 0 to 1 values, but is 2 values [tty]:1:1-12: cd dir1 dir2 ## GetHome error ## //mock-get-home-error can't get home ~> cd Exception: can't get home [tty]:1:1-2: cd elvish-0.21.0/pkg/eval/builtin_fn_io.d.elv000066400000000000000000000255601465720375400203730ustar00rootroot00000000000000# Takes arbitrary arguments and write them to the structured stdout. # # Examples: # # ```elvish-transcript # ~> put a # ▶ a # ~> put lorem ipsum [a b] [&k=v] # ▶ lorem # ▶ ipsum # ▶ [a b] # ▶ [&k=v] # ``` # # **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`. fn put {|@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). fn repeat {|n value| } # Reads `$n` bytes, or until end-of-file, and outputs the bytes as a string # value. The result may not be a valid UTF-8 string. # # Examples: # # ```elvish-transcript # ~> echo "a,b" | read-bytes 2 # ▶ 'a,' # ~> echo "a,b" | read-bytes 10 # ▶ "a,b\n" # ``` fn read-bytes {|n| } # 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 # ``` fn read-upto {|terminator| } # 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" # ``` fn read-line { } # Like `echo`, just without the newline. # # See also [`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. fn print {|&sep=' ' @value| } # Prints values to the byte stream according to a template. # # The template may contain "formatting verbs", sequences that start with `%`. # Each verb corresponds to an argument, and specifies how that argument will be # converted and formatted: # # - `%s`, `%q` and `%v` convert the argument to a string: # # - `%s`: use [to-string](#to-string) # - `%q`: use [repr](#repr) # - `%v`: equivalent to `%s` # - `%#v`: equivalent to `%q`. # # - `%t` converts the argument to a boolean using [bool](#bool), and prints it # as either `true` or `false`. # # - `%b`, `%c`, `%d`, `%o`, `%O`, `%x`, `%X` and `%U` convert the argument to an # integer, and then print it as follows: # # - `%b`: base 2 # - `%c`: the character represented by the Unicode codepoint # - `%d`: base 10 # - `%o`: base 8 # - `%O`: base 8, with `0o` prefix # - `%x`: base 16, with lower-case letters for a-f # - `%X`: base 16, with upper-case letters for A-F # - `%U`: Unicode format like U+1234; same as `U+%04X` # # - `%e`, `%E`, `%f`, `%F`, `%g` and `%G` convert the argument to a # floating-point number using [`inexact-num`](), and print it as follows: # # - `%e`: scientific notation like -1.234456e+78 # - `%E`: scientific notation like -1.234456E+78 # - `%f`: decimal point but no exponent, like 123.456 # - `%F`: same as `%f` # - `%g`: `%e` for large exponents, `%f` otherwise # - `%G`: `%E` for large exponents, `%F` otherwise # # - `%%` prints a literal `%` and consumes no argument. # # Unsupported verbs not documented don't cause exceptions, but the output will # contain content like `%!Z(unsupported formatting verb)`. # # Flags may be added between `%` and the verb to modify behavior like precision # and padding. See Go's [`fmt`](https://golang.org/pkg/fmt/#hdr-Printing) # package for details. # # This command does not add a trailing newline. Include it explicitly in the # template, like `printf "Hello %s\n" world`. # # Examples: # # ```elvish-transcript # ~> use math # ~> printf ": %10s %.3f\n" Pi $math:pi # printf ": %10s %.3f\n" E $math:e # : Pi 3.142 # : E 2.718 # ~> 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'] # ``` # # Since `printf` writes to the byte stream, capturing its output will generate # one value per line. To capture it into one string value, use it together with # [`slurp`](), as in `var s = (printf ... | slurp)`. # # **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). # # See also [`print`](), [`echo`](), [`pprint`](), and [`repr`](). fn printf {|template @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. # # See also [`print`](). # # Etymology: Bourne sh. fn echo {|&sep=' ' @value| } # Pretty-print representations of Elvish values. Examples: # # ```elvish-transcript # ~> pprint [foo bar] # [ # foo # bar # ] # ~> pprint [&k1=v1 &k2=v2] # [ # &k1= v1 # &k2= v2 # ] # ``` # # The output format is subject to change. # # See also [`repr`](). fn pprint {|@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" # ``` # # See also [`pprint`](). # # Etymology: [Python](https://docs.python.org/3/library/functions.html#repr). fn repr {|@value| } # 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 # ~> var e = ?(fail lorem-ipsum) # ~> show $e # Exception: lorem-ipsum # [tty]:1:11-26: var e = ?(fail lorem-ipsum) # ``` fn show {|e| } # Passes byte input to output, and discards value inputs. # # Example: # # ```elvish-transcript # ~> { put value; echo bytes } | only-bytes # bytes # ``` fn only-bytes { } # Passes value input to output, and discards byte inputs. # # Example: # # ```elvish-transcript # ~> { put value; echo bytes } | only-values # ▶ value # ``` fn only-values { } # 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). fn slurp { } # 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 # ``` # # See also [`from-terminated`](), [`read-upto`](), and [`to-lines`](). fn from-lines { } # 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. # # Numbers in JSON are parsed as follows: # # - Numbers without fractional parts are parsed as exact integers, with # support for arbitrary precision. # # - Numbers with fractional parts (even if it's `.0`) are parsed as # [inexact](language.html#exactness) floating-point numbers, and the parsing # may fail if the number can't be represented. # # 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] # ~> echo '[42, 100000000000000000000, 42.0, 42.2]' | from-json # ▶ [(num 42) (num 100000000000000000000) (num 42.0) (num 42.2)] # ``` # # See also [`to-json`](). fn from-json { } # 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 # ``` # # See also [`from-lines`](), [`read-upto`](), and [`to-terminated`](). fn from-terminated {|terminator| } # Writes each input to a separate line in the byte output. # # ```elvish-transcript # ~> put a b | to-lines # a # b # ~> to-lines [a b] # a # b # ``` # # See also [`from-lines`]() and [`to-terminated`](). fn to-lines {|inputs?| } # Writes each input to the byte output with the specified terminator character. # # The `$terminator` must be a single ASCII character such as `"\x00"` (NUL). # Using NUL is useful for feeding output to programs that expect NUL-terminated # strings, such as `grep -z` or `xargs -0`. # # ```elvish-transcript # ~> put a b | to-terminated "\x00" | slurp # ▶ "a\x00b\x00" # ~> to-terminated "\x00" [a b] | slurp # ▶ "a\x00b\x00" # ``` # # See also [`from-terminated`]() and [`to-lines`](). fn to-terminated {|terminator inputs?| } # 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"} # ``` # # See also [`from-json`](). fn to-json { } elvish-0.21.0/pkg/eval/builtin_fn_io.go000066400000000000000000000234111465720375400177610ustar00rootroot00000000000000package eval import ( "bufio" "encoding/json" "fmt" "io" "math/big" "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]any{ // Value output "put": put, "repeat": repeat, // Bytes input "read-bytes": readBytes, "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, }) } func put(fm *Frame, args ...any) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(a) if err != nil { return err } } return nil } func repeat(fm *Frame, n int, v any) error { out := fm.ValueOutput() for i := 0; i < n; i++ { err := out.Put(v) if err != nil { return err } } return nil } func readBytes(fm *Frame, max int) (string, error) { in := fm.InputFile() buf := make([]byte, max) read := 0 for read < max { n, err := in.Read(buf[read:]) read += n if err == io.EOF { break } else if err != nil { return "", err } } return string(buf[:read]), nil } 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 } func readLine(fm *Frame) (string, error) { s, err := readUpto(fm, "\n") if err != nil { return "", err } return strutil.ChopLineEnding(s), nil } type printOpts struct{ Sep string } func (o *printOpts) SetDefaultOptions() { o.Sep = " " } func print(fm *Frame, opts printOpts, args ...any) 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 } func printf(fm *Frame, template string, args ...any) error { wrappedArgs := make([]any, len(args)) for i, arg := range args { wrappedArgs[i] = formatter{arg} } _, err := fmt.Fprintf(fm.ByteOutput(), template, wrappedArgs...) return err } type formatter struct { wrapped any } 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.ReprPlain(wrapped)) case 'v': var s string if state.Flag('#') { s = vals.ReprPlain(wrapped) } 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 any) { // 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) } func echo(fm *Frame, opts printOpts, args ...any) error { err := print(fm, opts, args...) if err != nil { return err } _, err = fm.ByteOutput().WriteString("\n") return err } func pprint(fm *Frame, args ...any) 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 } func repr(fm *Frame, args ...any) error { out := fm.ByteOutput() for i, arg := range args { if i > 0 { _, err := out.WriteString(" ") if err != nil { return err } } _, err := out.WriteString(vals.ReprPlain(arg)) if err != nil { return err } } _, err := out.WriteString("\n") return err } 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 } 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 } 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 } func slurp(fm *Frame) (string, error) { b, err := io.ReadAll(fm.InputFile()) return string(b), err } 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 } } } func fromJSON(fm *Frame) error { in := fm.InputFile() out := fm.ValueOutput() dec := json.NewDecoder(in) // See comments below about using json.Number. dec.UseNumber() for { var v any 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 any) (any, error) { switch v := v.(type) { case nil, bool, string: return v, nil case json.Number: // The JSON syntax doesn't restrict the precision of numbers. Since // we called json.Decoder.UseNumber, it preserves the full number // literal, and we can try parsing it as a big int. if z, ok := new(big.Int).SetString(v.String(), 0); ok { // Also normalize to int if the value fits. return vals.NormalizeBigInt(z), nil } // Parse as float64 instead. This can error if the number is not an // integer and exceeds the range of float64. return strconv.ParseFloat(v.String(), 64) case float64: return v, nil case []any: vec := vals.EmptyList for _, elem := range v { converted, err := fromJSONInterface(elem) if err != nil { return nil, err } vec = vec.Conj(converted) } return vec, nil case map[string]any: 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) } } 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 } } } func toLines(fm *Frame, inputs Inputs) error { out := fm.ByteOutput() var errOut error inputs(func(v any) { if errOut != nil { return } // TODO: Don't ignore the error. _, errOut = fmt.Fprintln(out, vals.ToString(v)) }) return errOut } 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 any) { if errOut != nil { return } _, errOut = fmt.Fprint(out, vals.ToString(v), terminator) }) return errOut } func toJSON(fm *Frame, inputs Inputs) error { encoder := json.NewEncoder(fm.ByteOutput()) var errEncode error inputs(func(v any) { if errEncode != nil { return } errEncode = encoder.Encode(v) }) return errEncode } elvish-0.21.0/pkg/eval/builtin_fn_io_test.elvts000066400000000000000000000160641465720375400215560ustar00rootroot00000000000000/////// # put # /////// ~> put foo bar ▶ foo ▶ bar ~> put $nil ▶ $nil // bubbling output error ~> put foo >&- Exception: port does not support value output [tty]:1:1-11: put foo >&- ////////// # repeat # ////////// ~> repeat 4 foo ▶ foo ▶ foo ▶ foo ▶ foo // bubbling output error ~> repeat 1 foo >&- Exception: port does not support value output [tty]:1:1-16: repeat 1 foo >&- ////////////// # read-bytes # ////////////// ~> print abcd | read-bytes 1 ▶ a // read-bytes does not consume more than needed ## ~> print abcd | { read-bytes 1; slurp } ▶ a ▶ bcd // reads up to EOF ## ~> print abcd | read-bytes 10 ▶ abcd // bubbling output error ~> print abcd | read-bytes 1 >&- Exception: port does not support value output [tty]:1:14-29: print abcd | read-bytes 1 >&- ///////////// # read-upto # ///////////// ~> print abcd | read-upto c ▶ abc // read-upto does not consume more than needed ## ~> print abcd | { read-upto c; slurp } ▶ abc ▶ d // read-upto reads up to EOF ## ~> print abcd | read-upto z ▶ abcd // bad terminator ~> print abcd | read-upto cd Exception: bad value: terminator must be a single ASCII character, but is cd [tty]:1:14-25: print abcd | read-upto cd // bubbling output error ~> print abcd | read-upto c >&- Exception: port does not support value output [tty]:1:14-28: print abcd | read-upto c >&- ///////////// # read-line # ///////////// ~> print eof-ending | read-line ▶ eof-ending ~> print "lf-ending\n" | read-line ▶ lf-ending ~> print "crlf-ending\r\n" | read-line ▶ crlf-ending ~> print "extra-cr\r\r\n" | read-line ▶ "extra-cr\r" // bubbling output error ~> print eof-ending | read-line >&- Exception: port does not support value output [tty]:1:20-32: print eof-ending | read-line >&- ///////// # print # ///////// ~> print [foo bar] ; print "\n" [foo bar] ~> print foo bar &sep=, ; print "\n" foo,bar // bubbling output error ~> print foo >&- Exception: invalid argument [tty]:1:1-13: print foo >&- //////// # echo # //////// ~> echo [foo bar] [foo bar] // bubbling output error ~> echo foo >&- Exception: invalid argument [tty]:1:1-12: echo foo >&- ////////// # pprint # ////////// ~> pprint [foo bar] [ foo bar ] // bubbling output error ~> pprint foo >&- Exception: invalid argument [tty]:1:1-14: pprint foo >&- //////// # repr # //////// ~> repr foo bar ['foo bar'] foo bar ['foo bar'] // bubbling output error ~> repr foo >&- Exception: invalid argument [tty]:1:1-12: repr foo >&- //////// # show # //////// ~> var exc = ?(fail foo) echo 'Showing exception:' show $exc Showing exception: Exception: foo [tty]:1:13-20: var exc = ?(fail foo) // bubbling output error ~> repr ?(fail foo) >&- Exception: invalid argument [tty]:1:1-20: repr ?(fail foo) >&- ////////////// # only-bytes # ////////////// ~> { echo bytes; put values } | only-bytes bytes // bubbling output error ~> { print bytes; put values } | only-bytes >&- Exception: invalid argument [tty]:1:31-44: { print bytes; put values } | only-bytes >&- /////////////// # only-values # /////////////// ~> { echo bytes; put values } | only-values ▶ values // bubbling output error ~> { print bytes; put values } | only-values >&- Exception: port does not support value output [tty]:1:31-45: { print bytes; put values } | only-values >&- ///////// # slurp # ///////// ~> print "a\nb" | slurp ▶ "a\nb" // bubbling output error ~> print "a\nb" | slurp >&- Exception: port does not support value output [tty]:1:16-24: print "a\nb" | slurp >&- ////////////// # from-lines # ////////////// ~> print "a\nb" | from-lines ▶ a ▶ b ~> print "a\nb\n" | from-lines ▶ a ▶ b // bubbling output error ~> print "a\nb\n" | from-lines >&- Exception: port does not support value output [tty]:1:18-31: print "a\nb\n" | from-lines >&- //////////// # to-lines # //////////// ~> put "l\norem" ipsum | to-lines l orem ipsum // bubbling output error ~> to-lines [foo] >&- Exception: invalid argument [tty]:1:1-18: to-lines [foo] >&- /////////////////// # from-terminated # /////////////////// ~> print "a\nb\x00\x00c\x00d" | from-terminated "\x00" ▶ "a\nb" ▶ '' ▶ c ▶ d ~> print "a\x00b\x00" | from-terminated "\x00" ▶ a ▶ b ~> print aXbXcXXd | from-terminated "X" ▶ a ▶ b ▶ c ▶ '' ▶ d // bad argument ~> from-terminated "xyz" Exception: bad value: terminator must be a single ASCII character, but is xyz [tty]:1:1-21: from-terminated "xyz" // bubbling output error ~> print aXbX | from-terminated X >&- Exception: port does not support value output [tty]:1:14-34: print aXbX | from-terminated X >&- ///////////////// # to-terminated # ///////////////// ~> put "l\norem" ipsum | to-terminated "\x00" | slurp ▶ "l\norem\x00ipsum\x00" ~> to-terminated "X" [a b c] ; print "\n" aXbXcX ~> to-terminated "XYZ" [a b c] Exception: bad value: terminator must be a single ASCII character, but is XYZ [tty]:1:1-27: to-terminated "XYZ" [a b c] // bubbling output error ~> to-terminated "X" [a b c] >&- Exception: invalid argument [tty]:1:1-29: to-terminated "X" [a b c] >&- ///////////// # from-json # ///////////// ~> echo '{"k": "v", "a": [1, 2]}' '"foo"' | from-json ▶ [&a=[(num 1) (num 2)] &k=v] ▶ foo ~> echo '[null, "foo"]' | from-json ▶ [$nil foo] // Numbers greater than 2^63 are supported ~> echo 100000000000000000000 | from-json ▶ (num 100000000000000000000) // Numbers with fractional parts become float64 ~> echo 1.0 | from-json ▶ (num 1.0) ~> echo 'invalid' | from-json Exception: invalid character 'i' looking for beginning of value [tty]:1:18-26: echo 'invalid' | from-json // bubbling output error ~> echo '[]' | from-json >&- Exception: port does not support value output [tty]:1:13-25: echo '[]' | from-json >&- /////////// # to-json # /////////// ~> put [&k=v &a=[1 2]] foo | to-json {"a":["1","2"],"k":"v"} "foo" ~> put [$nil foo] | to-json [null,"foo"] // bubbling output error ~> to-json [foo] >&- Exception: invalid argument [tty]:1:1-17: to-json [foo] >&- ////////// # printf # ////////// ~> printf "abcd\n" abcd ~> printf "%s\n%s\n" abc xyz abc xyz // %q uses repr ~> printf "%q\n" "abc xyz" 'abc xyz' ~> printf "%q\n" ['a b'] ['a b'] // %v uses to-string ~> printf "%v\n" abc abc // %#v is the same as %q ~> printf "%#v\n" "abc xyz" 'abc xyz' // width and precision ~> printf "%5.3s\n" 3.1415 3.1 ~> printf "%5.3s\n" (num 3.1415) 3.1 // %t converts to bool ~> printf "%t\n" $true true ~> printf "%t\n" $nil false // %d and %b convert to integer ~> printf "%3d\n" (num 5) 5 ~> printf "%3d\n" 5 5 ~> printf "%08b\n" (num 5) 00000101 ~> printf "%08b\n" 5 00000101 // %f converts to float64 ~> printf "%.1f\n" 3.1415 3.1 ~> printf "%.1f\n" (num 3.1415) 3.1 // does not interpret escape sequences ~> printf '%s\n%s\n' abc xyz ; print "\n" abc\nxyz\n // float verb with argument that can't be converted to float ~> printf "%f\n" 1.3x %!f(cannot parse as number: 1.3x) // integer verb with argument that can't be converted to integer ~> printf "%d\n" 3.5 %!d(cannot parse as integer: 3.5) // unsupported verb ~> printf "%A\n" foo %!A(unsupported formatting verb) // bubbling output error ~> printf foo >&- Exception: invalid argument [tty]:1:1-14: printf foo >&- elvish-0.21.0/pkg/eval/builtin_fn_misc.d.elv000066400000000000000000000122551465720375400207140ustar00rootroot00000000000000# 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). fn nop {|&any-opt= @value| } # Output the kinds of `$value`s. Example: # # ```elvish-transcript # ~> kind-of lorem [] [&] # ▶ string # ▶ list # ▶ map # ``` # # The concept of "kind" can be thought of as an approximation of type, but it's # not very well-defined. It's subject to change. fn kind-of {|@value| } # Output a function that takes no arguments and outputs `$value`s when called. # Examples: # # ```elvish-transcript # ~> var f = (constantly lorem ipsum) # ~> $f # ▶ lorem # ▶ ipsum # ``` # # The above example is equivalent to simply `var f = { put lorem ipsum }`; # it is most useful when the argument is **not** a literal value, e.g. # # ```elvish-transcript # //eval fn whoami { echo elf } # ~> var f = (constantly (whoami)) # ~> $f # ▶ elf # ~> $f # ▶ elf # ``` # # The above code only calls `whoami` once when defining `$f`. In contrast, if # `$f` is defined as `var f = { put (whoami) }`, every time you invoke `$f`, # `whoami` will be called. # # Etymology: [Clojure](https://clojuredocs.org/clojure.core/constantly). fn constantly {|@value| } # Calls `$fn` with `$args` as the arguments, and `$opts` as the option. Useful # for calling a function with dynamic option keys. # # Example: # # ```elvish-transcript # ~> var f = {|a &k1=v1 &k2=v2| put $a $k1 $k2 } # ~> call $f [foo] [&k1=bar] # ▶ foo # ▶ bar # ▶ v2 # ``` fn call {|fn args opts| } # 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 # ▶ '$echo~' # ~> fn f { } # ~> resolve f # ▶ '$f~' # ~> resolve cat # ▶ '(external cat)' # ``` fn resolve {|command| } # 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](language.html#upvalues) 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 # ~> var x = foo # ~> eval 'put $x' # ▶ foo # ~> var ns = (ns [&x=bar]) # ~> eval &ns=$ns 'put $x' # ▶ bar # ``` # # Examples that modify existing variables: # # ```elvish-transcript # ~> var y = foo # ~> eval 'set y = bar' # ~> put $y # ▶ bar # ``` # # Examples that creates new variables and uses the callback to access it: # # ```elvish-transcript # ~> eval 'var z = lorem' # ~> put $z # Compilation error: variable $z not found # [tty]:1:5-6: put $z # ~> var saved-ns = $nil # ~> eval &on-end={|ns| set saved-ns = $ns } 'var z = lorem' # ~> put $saved-ns[z] # ▶ lorem # ``` # # Note that when using variables from an outer scope, only those # that have been referenced are captured as upvalues (see [closure # semantics](language.html#closure-semantics)) and thus accessible to `eval`: # # ```elvish-transcript # //skip-test # // Skipping since the error contains the context-sensitive "[eval 2]" # ~> var a b # ~> fn f {|code| nop $a; eval $code } # ~> f 'echo $a' # $nil # ~> f 'echo $b' # Exception: Compilation error: variable $b not found # [eval 2]:1:6-7: echo $b # [tty]:1:22-32: fn f {|code| nop $a; eval $code } # [tty]:1:1-11: f 'echo $b' # ``` fn eval {|code &ns=$nil &on-end=$nil| } #//in-temp-dir # 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 'var x = value' > a.elv # ~> put (use-mod ./a)[x] # ▶ value # ``` fn use-mod {|use-spec| } # 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 # [tty]:1:1-13: deprecate msg # ~> fn f { deprecate msg } # ~> f # Deprecation: msg # [tty]:1:1-1: f # ~> f # a different call site; shows deprecate message # Deprecation: msg # [tty]:1:1-50: f # a different call site; shows deprecate message # ~> fn g { f } # ~> g # Deprecation: msg # [tty]:1:8-9: fn g { f } # ~> g # same call site, no more deprecation message # ``` fn deprecate {|msg| } #doc:show-unstable # Output all IP addresses of the current host. # # This should be part of a networking module instead of the builtin module. fn -ifaddrs { } elvish-0.21.0/pkg/eval/builtin_fn_misc.go000066400000000000000000000065031465720375400203100ustar00rootroot00000000000000package eval // Misc builtin functions. import ( "errors" "fmt" "net" "sync" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/errs" "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]any{ "kind-of": kindOf, "constantly": constantly, // Introspection "call": call, "resolve": resolve, "eval": eval, "use-mod": useMod, "deprecate": deprecate, "-ifaddrs": _ifaddrs, }) } var nopGoFn = NewGoFn("nop", nop) func nop(opts RawOptions, args ...any) { // Do nothing } func kindOf(fm *Frame, args ...any) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(vals.Kind(a)) if err != nil { return err } } return nil } func constantly(args ...any) 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 }, ) } func call(fm *Frame, fn Callable, argsVal vals.List, optsVal vals.Map) error { args := make([]any, 0, argsVal.Len()) for it := argsVal.Iterator(); it.HasElem(); it.Next() { args = append(args, it.Elem()) } opts := make(map[string]any, optsVal.Len()) for it := optsVal.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() ks, ok := k.(string) if !ok { return errs.BadValue{What: "option key", Valid: "string", Actual: vals.Kind(k)} } opts[ks] = v } return fn.Call(fm.Fork(), args, opts) } 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) + ")" } } 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() errCb := opts.OnEnd.Call(newFm, []any{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 nextEvalCount = nextEvalCountImpl ) func nextEvalCountImpl() int { evalCountMutex.Lock() defer evalCountMutex.Unlock() evalCount++ return evalCount } func useMod(fm *Frame, spec string) (*Ns, error) { return use(fm, spec, nil) } 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) } 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.21.0/pkg/eval/builtin_fn_misc_test.elvts000066400000000000000000000070111465720375400220720ustar00rootroot00000000000000/////////// # kind-of # /////////// ~> kind-of a [] ▶ string ▶ list // bubbling output error ~> kind-of a >&- Exception: port does not support value output [tty]:1:1-13: kind-of a >&- ////////////// # constantly # ////////////// ~> var f = (constantly foo) $f ▶ foo ~> $f ▶ foo // bubbling output error ~> (constantly foo) >&- Exception: port does not support value output [tty]:1:1-20: (constantly foo) >&- //////// # call # //////// ~> call {|arg &opt=v| put $arg $opt } [foo] [&opt=bar] ▶ foo ▶ bar ~> call { } [foo] [&[]=bar] Exception: bad value: option key must be string, but is list [tty]:1:1-24: call { } [foo] [&[]=bar] //////// # eval # //////// ~> eval 'put x' ▶ x ## using variable from the local scope ## ~> var x = foo eval 'put $x' ▶ foo ## setting a variable in the local scope ## ~> var x = foo eval 'set x = bar' put $x ▶ bar ## using variable from the upvalue scope ## ~> var x = foo { nop $x; eval 'put $x' } ▶ foo ## using &ns to specify a namespace ## ~> var n = (ns [&x=foo]) eval 'put $x' &ns=$n ▶ foo ## altering variables in the specified namespace ## ~> var n = (ns [&x=foo]) eval 'set x = bar' &ns=$n put $n[x] ▶ bar ## newly created variables do not appear in the local namespace ## ~> eval 'x = foo' put $x Compilation error: variable $x not found [tty]:2:5-6: put $x ## newly created variables do not alter the specified namespace either ## ~> var n = (ns [&]) eval &ns=$n 'var x = foo' put $n[x] Exception: no such key: x [tty]:3:5-9: put $n[x] ## newly created variable can be accessed in the final namespace using &on-end ## ~> eval &on-end={|n| put $n[x] } 'var x = foo' ▶ foo ## parse error ## //force-eval-source-count 100 ~> eval '[' Exception: Parse error: should be ']' [eval 100]:1:2: [ [tty]:1:1-8: eval '[' ## compilation error ## //force-eval-source-count 100 ~> eval 'put $x' Exception: Compilation error: variable $x not found [eval 100]:1:5-6: put $x [tty]:1:1-13: eval 'put $x' ## exception ## //force-eval-source-count 100 ~> eval 'fail x' Exception: x [eval 100]:1:1-6: fail x [tty]:1:1-13: eval 'fail x' ///////////// # deprecate # ///////////// ~> deprecate msg Deprecation: msg [tty]:1:1-13: deprecate msg ## different call sites trigger multiple deprecation messages ## ~> fn f { deprecate msg } ~> f Deprecation: msg [tty]:1:1-1: f // Normally, just calling f from the next prompt will result in a different call // site because the source will have a different name like "[tty 3]" vs "[tty // 2]". But since in tests we always use "[tty]" for the source name, we need to // make the call appear on a different position to force it to be recognized as // a different call site. ~> nop; f Deprecation: msg [tty]:1:6-6: nop; f ## the same call site only triggers the message once ## ~> fn f { deprecate msg} fn g { f } ~> g Deprecation: msg [tty]:2:8-9: fn g { f } // See comment above about call site. In this case, the (immediate) call site of // f is from g, so even if the call site of g differs, the call site of f is the // same. ~> nop; g /////////// # use-mod # /////////// //tmp-lib-dir ~> echo 'var x = value' > $lib/mod.elv ~> put (use-mod mod)[x] ▶ value /////////// # resolve # /////////// ~> resolve for ▶ special ~> resolve put ▶ '$put~' ~> fn f { } resolve f ▶ '$f~' // Unknown commands resolve to an external even if it doesn't exist. ~> resolve cat ▶ '(external cat)' ## module function ## //tmp-lib-dir ~> echo 'fn func { }' > $lib/mod.elv ~> use mod ~> resolve mod:func ▶ '$mod:func~' elvish-0.21.0/pkg/eval/builtin_fn_num.d.elv000066400000000000000000000236451465720375400205650ustar00rootroot00000000000000#//skip-test # Output a pseudo-random number in the interval [0, 1). Example: # # ```elvish-transcript # ~> rand # ▶ 0.17843564133528436 # ``` fn rand { } # 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) # ``` # # See also [`exact-num`]() and [`inexact-num`](). fn 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) # ``` # # See also [`num`]() and [`inexact-num`](). fn exact-num {|string-or-number| } # Coerces the argument to an inexact number. # # If the argument is a string, it is converted to a typed number first. If the # argument is already an inexact number, it is returned as is. # # Examples: # # ```elvish-transcript # ~> inexact-num (num 1) # ▶ (num 1.0) # ~> inexact-num (num 0.5) # ▶ (num 0.5) # ~> inexact-num (num 1/2) # ▶ (num 0.5) # ~> inexact-num 1/2 # ▶ (num 0.5) # ``` # # Since the underlying representation for inexact numbers has limited range, # numbers with very large magnitudes may be converted to an infinite value: # # ```elvish-transcript # ~> inexact-num 1000000000000000000 # ▶ (num 1e+18) # ~> inexact-num 10000000000000000000 # ▶ (num +Inf) # ~> inexact-num -10000000000000000000 # ▶ (num -Inf) # ``` # # Likewise, numbers with very small magnitudes may be converted to 0: # # ```elvish-transcript # ~> use math # ~> math:pow 10 -323 # ▶ (num 1/100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) # ~> inexact-num (math:pow 10 -323) # ▶ (num 1e-323) # ~> math:pow 10 -324 # ▶ (num 1/1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) # ~> inexact-num (math:pow 10 -324) # ▶ (num 0.0) # ``` # # See also [`num`]() and [`exact-num`](). fn inexact-num {|string-or-number| } #doc:html-id num-lt # Outputs whether `$number`s in the given order are numerically strictly # increasing. Outputs `$true` when given fewer than two numbers. # # Examples: # # ```elvish-transcript # ~> < 1 2 # ▶ $true # ~> < 2 1 # ▶ $false # ~> < 1 2 3 # ▶ $true # ``` # fn '<' {|@number| } #doc:html-id num-le # Outputs whether `$number`s in the given order are numerically non-decreaing. # Outputs `$true` when given fewer than two numbers. # # Examples: # # ```elvish-transcript # ~> <= 1 1 # ▶ $true # ~> <= 2 1 # ▶ $false # ~> <= 1 1 2 # ▶ $true # ``` fn '<=' {|@number| } #doc:html-id num-eq # Outputs whether `$number`s are all numerically equal. Outputs `$true` when # given fewer than two numbers. # # Examples: # # ```elvish-transcript # ~> == 1 1 # ▶ $true # ~> == 1 (num 1) # ▶ $true # ~> == 1 (num 1) 1 # ▶ $true # ~> == 1 (num 1) 1.0 # ▶ $true # ~> == 1 2 # ▶ $false # ``` fn '==' {|@number| } #doc:html-id num-ne # Determines whether `$a` and `$b` are numerically inequal. Equivalent to `not # (== $a $b)`. # # Examples: # # ```elvish-transcript # ~> != 1 2 # ▶ $true # ~> != 1 1 # ▶ $false # ``` fn '!=' {|a b| } #doc:html-id num-gt # Determines whether `$number`s in the given order are numerically strictly # decreasing. Outputs `$true` when given fewer than two numbers. # # Examples: # # ```elvish-transcript # ~> > 2 1 # ▶ $true # ~> > 1 2 # ▶ $false # ~> > 3 2 1 # ▶ $true # ``` fn '>' {|@number| } #doc:html-id num-ge # Outputs whether `$number`s in the given order are numerically non-increasing. # Outputs `$true` when given fewer than two numbers. # # Examples: # # ```elvish-transcript # ~> >= 1 1 # ▶ $true # ~> >= 1 2 # ▶ $false # ~> >= 2 1 1 # ▶ $true # ``` fn '>=' {|@number| } #doc:html-id add # 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) # ``` fn + {|@num| } #doc:html-id sub # 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) # ``` fn - {|x-num @y-num| } #doc:html-id mul # 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) # ``` fn * {|@num| } #doc:html-id div # 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]:1:1-5: / 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 is deprecated since 0.21.0). fn / {|x-num @y-num| } #doc:html-id rem # Outputs the remainder after dividing `$x` by `$y`. The result has the same # sign as `$x`. Both arguments must be exact integers. # # Examples: # # ```elvish-transcript # ~> % 10 3 # ▶ (num 1) # ~> % -10 3 # ▶ (num -1) # ~> % 10 -3 # ▶ (num 1) # ~> % 10000000000000000000 3 # ▶ (num 1) # ~> % 10.0 3 # Exception: bad value: argument must be exact integer, but is (num 10.0) # [tty]:1:1-8: % 10.0 3 # ``` # # This limit may be lifted in the future. fn % {|x y| } #//skip-test # 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 # ``` fn randint {|low? high| } #doc:show-unstable # Sets the seed for the random number generator. fn -randseed {|seed| } # 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) # ~> use math # ~> 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) # ``` # # One usage of this command is to execute something a fixed number of times by # combining with [each](#each): # # ```elvish-transcript # ~> range 3 | each {|_| echo foo } # foo # foo # foo # ``` # # Etymology: # [Python](https://docs.python.org/3/library/functions.html#func-range). fn range {|&step start=0 end| } elvish-0.21.0/pkg/eval/builtin_fn_num.go000066400000000000000000000301051465720375400201470ustar00rootroot00000000000000package eval import ( "fmt" "math" "math/big" "math/rand" "strconv" "sync" "time" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Numerical operations. func init() { addBuiltinFns(map[string]any{ // Constructor "num": num, "exact-num": exactNum, "inexact-num": inexactNum, // Comparison "<": lt, "<=": le, "==": eqNum, "!=": ne, ">": gt, ">=": ge, // Arithmetic "+": add, "-": sub, "*": mul, // Also handles cd / "/": slash, "%": rem, // Random "rand": randFn, "randint": randint, "-randseed": randseed, "range": rangeFn, }) } func num(n vals.Num) vals.Num { // Conversion is actually handled in vals/conversion.go. return n } 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 } func inexactNum(f float64) float64 { return f } func lt(nums ...vals.Num) bool { return chainCompareNums(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 chainCompareNums(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 chainCompareNums(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(a, b vals.Num) bool { return unifyNums2And(a, b, 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 chainCompareNums(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 chainCompareNums(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 chainCompareNums(nums []vals.Num, pInt func(a, b int) bool, pBigInt func(a, b *big.Int) bool, pBigRat func(a, b *big.Rat) bool, pFloat64 func(a, b float64) bool) bool { for i := 0; i < len(nums)-1; i++ { r := unifyNums2And(nums[i], nums[i+1], pInt, pBigInt, pBigRat, pFloat64) if !r { return false } } return true } func unifyNums2And[T any](a, b vals.Num, fInt func(a, b int) T, fBigInt func(a, b *big.Int) T, fBigRat func(a, b *big.Rat) T, fFloat64 func(a, b float64) T) T { a, b = vals.UnifyNums2(a, b, 0) switch a := a.(type) { case int: return fInt(a, b.(int)) case *big.Int: return fBigInt(a, b.(*big.Int)) case *big.Rat: return fBigRat(a, b.(*big.Rat)) case float64: return fFloat64(a, b.(float64)) default: panic("unreachable") } } 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") } } 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") } } 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") } } func slash(fm *Frame, args ...vals.Num) error { if len(args) == 0 { fm.Deprecate("implicit cd is deprecated; use cd or location mode instead", fm.traceback.Head, 21) // 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") } } func rem(a, b vals.Num) (vals.Num, error) { if err := checkExactIntArg(a); err != nil { return 0, err } if err := checkExactIntArg(b); err != nil { return 0, err } if b == 0 { return 0, ErrDivideByZero } if a, ok := a.(int); ok { if b, ok := b.(int); ok { return a % b, nil } } return new(big.Int).Rem(vals.PromoteToBigInt(a), vals.PromoteToBigInt(b)), nil } func checkExactIntArg(a vals.Num) error { switch a.(type) { case int, *big.Int: return nil default: return errs.BadValue{What: "argument", Valid: "exact integer", Actual: vals.ReprPlain(a)} } } func randFn() float64 { return withRand((*rand.Rand).Float64) } func randint(args ...vals.Num) (vals.Num, error) { if len(args) == 0 || len(args) > 2 { return -1, errs.ArityMismatch{What: "arguments", ValidLow: 1, ValidHigh: 2, Actual: len(args)} } allInt := true for _, arg := range args { if err := checkExactIntArg(arg); err != nil { return nil, err } if _, ok := arg.(*big.Int); ok { allInt = false } } if allInt { var low, high int if len(args) == 1 { low, high = 0, args[0].(int) } else { low, high = args[0].(int), args[1].(int) } if high <= low { return 0, errs.BadValue{What: "high value", Valid: fmt.Sprint("larger than ", low), Actual: strconv.Itoa(high)} } x := withRand(func(r *rand.Rand) int { return r.Intn(high - low) }) return low + x, nil } var low, high *big.Int if len(args) == 1 { low, high = big.NewInt(0), args[0].(*big.Int) } else { low, high = args[0].(*big.Int), args[1].(*big.Int) } if high.Cmp(low) <= 0 { return 0, errs.BadValue{What: "high value", Valid: fmt.Sprint("larger than ", low), Actual: high.String()} } if low.Sign() == 0 { return withRand(func(r *rand.Rand) *big.Int { return new(big.Int).Rand(r, high) }), nil } else { diff := new(big.Int).Sub(high, low) z := withRand(func(r *rand.Rand) *big.Int { return new(big.Int).Rand(r, diff) }) z.Add(z, low) return z, nil } } func randseed(x int) { withRandNullary(func(r *rand.Rand) { r.Seed(int64(x)) }) } var ( randMutex sync.Mutex randInstance *rand.Rand ) func withRand[T any](f func(*rand.Rand) T) T { randMutex.Lock() defer randMutex.Unlock() if randInstance == nil { randInstance = rand.New(rand.NewSource(time.Now().UnixNano())) } return f(randInstance) } func withRandNullary(f func(*rand.Rand)) { withRand(func(r *rand.Rand) struct{} { f(r) return struct{}{} }) } 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 rangeBuiltinNum(nums, out) case []*big.Int: return rangeBigNum(nums, out, bigIntDesc) case []*big.Rat: return rangeBigNum(nums, out, bigRatDesc) case []float64: return rangeBuiltinNum(nums, out) default: panic("unreachable") } } type builtinNum interface{ int | float64 } func rangeBuiltinNum[T builtinNum](nums []T, out ValueOutput) error { start, end := nums[0], nums[1] var step T 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 } type bigNum[T any] interface { Cmp(T) int Sign() int Add(T, T) T } type bigNumDesc[T any] struct { one T negOne T newZero func() T } var bigIntDesc = bigNumDesc[*big.Int]{ one: big.NewInt(1), negOne: big.NewInt(-1), newZero: func() *big.Int { return &big.Int{} }, } var bigRatDesc = bigNumDesc[*big.Rat]{ one: big.NewRat(1, 1), negOne: big.NewRat(-1, 1), newZero: func() *big.Rat { return &big.Rat{} }, } func rangeBigNum[T bigNum[T]](nums []T, out ValueOutput, d bigNumDesc[T]) error { start, end := nums[0], nums[1] var step T 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 = d.one } var cur, next T for cur = start; cur.Cmp(end) < 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = d.newZero() 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 = d.negOne } var cur, next T for cur = start; cur.Cmp(end) > 0; cur = next { err := out.Put(vals.FromGo(cur)) if err != nil { return err } next = d.newZero() next.Add(cur, step) cur = next } } return nil } elvish-0.21.0/pkg/eval/builtin_fn_num_test.elvts000066400000000000000000000314701465720375400217440ustar00rootroot00000000000000// When testing numbers with different underlying types, the order is usually // // 1. int // 2. *big.Int (100000000000000000000 is often used) // 3. *big.Rat // 4. float64 /////// # num # /////// ~> num 1 ▶ (num 1) ~> num 100000000000000000000 ▶ (num 100000000000000000000) ~> num 1/2 ▶ (num 1/2) ~> num 0.1 ▶ (num 0.1) ~> num (num 1) ▶ (num 1) ///////////// # exact-num # ///////////// ~> exact-num 1 ▶ (num 1) ~> exact-num 0.125 ▶ (num 1/8) ~> exact-num inf Exception: bad value: argument here must be finite float, but is +Inf [tty]:1:1-13: exact-num inf /////////////// # inexact-num # /////////////// ~> inexact-num 1 ▶ (num 1.0) ~> inexact-num 1.0 ▶ (num 1.0) ~> inexact-num (num 1) ▶ (num 1.0) ~> inexact-num (num 1.0) ▶ (num 1.0) ////////////// # Comparison # ////////////// ## < ## // int ~> < 1 2 3 ▶ $true ~> < 1 3 2 ▶ $false // *big.Int ~> < 100000000000000000001 100000000000000000002 100000000000000000003 ▶ $true ~> < 100000000000000000001 100000000000000000003 100000000000000000002 ▶ $false // *big.Rat ~> < 1/4 1/3 1/2 ▶ $true ~> < 1/4 1/2 1/3 ▶ $false // float64 ~> < 1.0 2.0 3.0 ▶ $true ~> < 1.0 3.0 2.0 ▶ $false ## mixed types ## // Only test with <; the other commands share the same code path for handling // mixed types. // mixing int and *big.Int ~> < 1 100000000000000000001 ▶ $true // mixing int, *big.Int and *big.Rat ~> < 1/2 1 100000000000000000001 ▶ $true ~> < 1/2 100000000000000000001 1 ▶ $false // mixing int, *big.Rat and float64 ~> < 1.0 3/2 2 ▶ $true ~> < 1.0 2 3/2 ▶ $false ## <= ## // int ~> <= 1 1 2 ▶ $true ~> <= 1 2 1 ▶ $false // *big.Int ~> <= 100000000000000000001 100000000000000000001 100000000000000000002 ▶ $true ~> <= 100000000000000000001 100000000000000000002 100000000000000000001 ▶ $false // *big.Rat ~> <= 1/3 1/3 1/2 ▶ $true ~> <= 1/3 1/2 1/1 ▶ $true // float64 ~> <= 1.0 1.0 2.0 ▶ $true ~> <= 1.0 2.0 1.0 ▶ $false ## == ## // int ~> == 1 1 1 ▶ $true ~> == 1 2 1 ▶ $false // *big.Int ~> == 100000000000000000001 100000000000000000001 100000000000000000001 ▶ $true ~> == 100000000000000000001 100000000000000000002 100000000000000000001 ▶ $false // *big.Rat ~> == 1/2 1/2 1/2 ▶ $true ~> == 1/2 1/3 1/2 ▶ $false // float64 ~> == 1.0 1.0 1.0 ▶ $true ~> == 1.0 2.0 1.0 ▶ $false ## != ## // int ~> != 1 2 ▶ $true ~> != 1 1 ▶ $false // *big.Int ~> != 100000000000000000001 100000000000000000002 ▶ $true ~> != 100000000000000000001 100000000000000000001 ▶ $false // *big.Rat ~> != 1/2 1/3 ▶ $true ~> != 1/2 1/2 ▶ $false // float64 ~> != 1.0 2.0 ▶ $true ~> != 1.0 1.0 ▶ $false // only accepts two arguments ~> != Exception: arity mismatch: arguments must be 2 values, but is 0 values [tty]:1:1-2: != ~> != 1 Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-4: != 1 ~> != 1 2 3 Exception: arity mismatch: arguments must be 2 values, but is 3 values [tty]:1:1-8: != 1 2 3 ## > ## // int ~> > 3 2 1 ▶ $true ~> > 3 1 2 ▶ $false // *big.Int ~> > 100000000000000000003 100000000000000000002 100000000000000000001 ▶ $true ~> > 100000000000000000003 100000000000000000001 100000000000000000002 ▶ $false // *big.Rat ~> > 1/2 1/3 1/4 ▶ $true ~> > 1/2 1/4 1/3 ▶ $false // float64 ~> > 3.0 2.0 1.0 ▶ $true ~> > 3.0 1.0 2.0 ▶ $false ## >= ## // int ~> >= 3 3 2 ▶ $true ~> >= 3 2 3 ▶ $false // *big.Int ~> >= 100000000000000000003 100000000000000000003 100000000000000000002 ▶ $true ~> >= 100000000000000000003 100000000000000000002 100000000000000000003 ▶ $false // *big.Rat ~> >= 1/2 1/2 1/3 ▶ $true ~> >= 1/2 1/3 1/2 ▶ $false // float64 ~> >= 3.0 3.0 2.0 ▶ $true ~> >= 3.0 2.0 3.0 ▶ $false //////////////////// # basic arithmetic # //////////////////// ## + ## // no argument ~> + ▶ (num 0) // int ~> + 233100 233 ▶ (num 233333) // *big.Int ~> + 100000000000000000000 100000000000000000001 ▶ (num 200000000000000000001) // *big.Rat ~> + 1/2 1/3 1/4 ▶ (num 13/12) // float64 ~> + 0.5 0.25 1.0 ▶ (num 1.75) ## mixing types ## // Only test with +; the other commands share the same code path for handling // mixed types. // int and *big.Int ~> + 1 2 100000000000000000000 ▶ (num 100000000000000000003) // int, *big,Int and *big.Rat ~> + 1/2 1/2 1 100000000000000000000 ▶ (num 100000000000000000002) // int, *big.Rat and float64 ~> + 0.5 1/4 1 ▶ (num 1.75) ## - ## // no argument is unsupported ~> - Exception: arity mismatch: arguments must be 1 or more values, but is 0 values [tty]:1:1-1: - // one argument is negation ~> - 233 ▶ (num -233) ~> - 100000000000000000000 ▶ (num -100000000000000000000) ~> - 1/2 ▶ (num -1/2) ~> - 1.0 ▶ (num -1.0) // int ~> - 20 10 2 ▶ (num 8) // *big.Int ~> - 200000000000000000003 100000000000000000001 ▶ (num 100000000000000000002) // *big.Rat ~> - 1/2 1/3 ▶ (num 1/6) // float64 ~> - 2.0 1.0 0.5 ▶ (num 0.5) ## * ## // no argument ~> * ▶ (num 1) // int ~> * 2 7 4 ▶ (num 56) // *big.Int ~> * 2 100000000000000000001 ▶ (num 200000000000000000002) // *big.Rat ~> * 1/2 1/3 ▶ (num 1/6) // float64 ~> * 2.0 0.5 1.75 ▶ (num 1.75) // 0 * non-infinity ~> * 0 1/2 1.0 ▶ (num 0) // 0 * infinity ~> * 0 +Inf ▶ (num NaN) ## / ## // one argument - inversion ~> / 2 ▶ (num 1/2) ~> / 100000000000000000000 ▶ (num 1/100000000000000000000) ~> / 2.0 ▶ (num 0.5) // int ~> / 233333 353 ▶ (num 661) ~> / 3 4 2 ▶ (num 3/8) // *big.Int ~> / 200000000000000000000 100000000000000000000 ▶ (num 2) ~> / 200000000000000000000 2 ▶ (num 100000000000000000000) ~> / 100000000000000000001 100000000000000000000 ▶ (num 100000000000000000001/100000000000000000000) // float64 ~> / 1.0 2.0 4.0 ▶ (num 0.125) ~> / 0 1/2 0.1 ▶ (num 0) // anything / 0 ~> / 0 0 Exception: bad value: divisor must be number other than exact 0, but is exact 0 [tty]:1:1-5: / 0 0 ~> / 1 0 Exception: bad value: divisor must be number other than exact 0, but is exact 0 [tty]:1:1-5: / 1 0 ~> / 1.0 0 Exception: bad value: divisor must be number other than exact 0, but is exact 0 [tty]:1:1-7: / 1.0 0 ## implicit cd with / ## //only-on unix //in-temp-dir //deprecation-level 21 ~> / Deprecation: implicit cd is deprecated; use cd or location mode instead [tty]:1:1-1: / ~> put $pwd ▶ / ## % ## ~> % 23 7 ▶ (num 2) ~> % 1 0 Exception: bad value: divisor must be number other than exact 0, but is exact 0 [tty]:1:1-5: % 1 0 // big int support ~> % 10000000000000000000 3 ▶ (num 1) // floating point is not supported, even if integer ~> % 10.0 3 Exception: bad value: argument must be exact integer, but is (num 10.0) [tty]:1:1-8: % 10.0 3 ~> % 10 3.0 Exception: bad value: argument must be exact integer, but is (num 3.0) [tty]:1:1-8: % 10 3.0 /////////// # randint # /////////// ~> randint 1 2 ▶ (num 1) ~> randint 1 ▶ (num 0) ~> var i = (randint 10 100) and (<= 10 $i) (< $i 100) ▶ $true ~> var i = (randint 10) and (<= 0 $i) (< $i 10) ▶ $true // big int is OK ~> var z = (num 10000000000000000000) ~> < (randint $z) $z ▶ $true ~> var low = $z var high = (+ $low 10) var i = (randint $low $high) and (<= $low $i) (< $i $high) ▶ $true ## argument checking ## ~> randint 2 1 Exception: bad value: high value must be larger than 2, but is 1 [tty]:1:1-11: randint 2 1 ~> var z = (num 10000000000000000000) randint (+ $z 10) $z Exception: bad value: high value must be larger than 10000000000000000010, but is 10000000000000000000 [tty]:2:1-20: randint (+ $z 10) $z ~> randint Exception: arity mismatch: arguments must be 1 to 2 values, but is 0 values [tty]:1:1-7: randint ~> randint 1 2 3 Exception: arity mismatch: arguments must be 1 to 2 values, but is 3 values [tty]:1:1-13: randint 1 2 3 ~> randint 1.0 Exception: bad value: argument must be exact integer, but is (num 1.0) [tty]:1:1-11: randint 1.0 ///////////// # -randseed # ///////////// //reseed-afterwards // -randseed makes randint deterministic ## ~> fn f { -randseed 0; randint 10 } eq (f) (f) ▶ $true // big int uses a different code path internally, so verify that generating big // int is also deterministic. ~> fn f { -randseed 0; randint 10000000000000000000 } eq (f) (f) ▶ $true // rand is also deterministic ~> fn f { -randseed 0; rand } eq (f) (f) ▶ $true ///////// # range # ///////// ## argument arity check ## ~> range Exception: arity mismatch: arguments must be 1 to 2 values, but is 0 values [tty]:1:1-5: range ~> range 0 1 2 Exception: arity mismatch: arguments must be 1 to 2 values, but is 3 values [tty]:1:1-11: range 0 1 2 ## int ## // counting up ~> range 3 ▶ (num 0) ▶ (num 1) ▶ (num 2) ~> range 1 3 ▶ (num 1) ▶ (num 2) // counting down ~> range -1 10 &step=3 ▶ (num -1) ▶ (num 2) ▶ (num 5) ▶ (num 8) ~> range 3 -3 ▶ (num 3) ▶ (num 2) ▶ (num 1) ▶ (num 0) ▶ (num -1) ▶ (num -2) // invalid step ~> range &step=-1 1 Exception: bad value: step must be positive, but is -1 [tty]:1:1-16: range &step=-1 1 ~> range &step=1 1 0 Exception: bad value: step must be negative, but is 1 [tty]:1:1-17: range &step=1 1 0 // bubbling output error ~> range 2 >&- Exception: port does not support value output [tty]:1:1-11: range 2 >&- ## near max/min of int ## // Values near the max/min of int need to be handled carefully to avoid // overflow. Instead of assuming int is 64-bit or 32-bit, test interesting // values for both cases. // 2^63-3 to 2^63-1 ~> range 9223372036854775805 9223372036854775807 ▶ (num 9223372036854775805) ▶ (num 9223372036854775806) ~> range 9223372036854775807 9223372036854775805 ▶ (num 9223372036854775807) ▶ (num 9223372036854775806) // -2^63 to -2^63+2 ~> range -9223372036854775808 -9223372036854775806 ▶ (num -9223372036854775808) ▶ (num -9223372036854775807) ~> range -9223372036854775806 -9223372036854775808 ▶ (num -9223372036854775806) ▶ (num -9223372036854775807) // 2^31-3 to 2^31-1 ~> range 2147483645 2147483647 ▶ (num 2147483645) ▶ (num 2147483646) ~> range 2147483647 2147483645 ▶ (num 2147483647) ▶ (num 2147483646) // -2^31 to -2^31+2 ~> range -2147483648 -2147483646 ▶ (num -2147483648) ▶ (num -2147483647) ~> range -2147483646 -2147483648 ▶ (num -2147483646) ▶ (num -2147483647) ## *big.Int ## // counting up ~> range 100000000000000000000 100000000000000000003 ▶ (num 100000000000000000000) ▶ (num 100000000000000000001) ▶ (num 100000000000000000002) ~> range 100000000000000000000 100000000000000000003 &step=2 ▶ (num 100000000000000000000) ▶ (num 100000000000000000002) // counting down ~> range 100000000000000000003 100000000000000000000 ▶ (num 100000000000000000003) ▶ (num 100000000000000000002) ▶ (num 100000000000000000001) ~> range 100000000000000000003 100000000000000000000 &step=-2 ▶ (num 100000000000000000003) ▶ (num 100000000000000000001) // invalid step ~> range &step=-100000000000000000000 10 Exception: bad value: step must be positive, but is -100000000000000000000 [tty]:1:1-37: range &step=-100000000000000000000 10 ~> range &step=100000000000000000000 10 0 Exception: bad value: step must be negative, but is 100000000000000000000 [tty]:1:1-38: range &step=100000000000000000000 10 0 // bubbling output error ~> range 100000000000000000000 100000000000000000001 >&- Exception: port does not support value output [tty]:1:1-53: range 100000000000000000000 100000000000000000001 >&- ## *big.Rat ## // counting up ~> range 23/10 ▶ (num 0) ▶ (num 1) ▶ (num 2) ~> range 1/10 23/10 ▶ (num 1/10) ▶ (num 11/10) ▶ (num 21/10) ~> range 1/10 9/10 &step=3/10 ▶ (num 1/10) ▶ (num 2/5) ▶ (num 7/10) // counting down ~> range 23/10 1/10 ▶ (num 23/10) ▶ (num 13/10) ▶ (num 3/10) ~> range 9/10 0/10 &step=-3/10 ▶ (num 9/10) ▶ (num 3/5) ▶ (num 3/10) // invalid step ~> range &step=-1/2 10 Exception: bad value: step must be positive, but is -1/2 [tty]:1:1-19: range &step=-1/2 10 ~> range &step=1/2 10 0 Exception: bad value: step must be negative, but is 1/2 [tty]:1:1-20: range &step=1/2 10 0 // bubbling output error ~> range 1/2 3/2 >&- Exception: port does not support value output [tty]:1:1-17: range 1/2 3/2 >&- ## float64 ## // counting up ~> range 1.2 ▶ (num 0.0) ▶ (num 1.0) ~> range &step=0.5 1 3 ▶ (num 1.0) ▶ (num 1.5) ▶ (num 2.0) ▶ (num 2.5) // counting down ~> range 1.2 -1.2 ▶ (num 1.2) ▶ (num 0.19999999999999996) ▶ (num -0.8) ~> range &step=-0.5 3 1 ▶ (num 3.0) ▶ (num 2.5) ▶ (num 2.0) ▶ (num 1.5) // Nearing the maximum float64 value where x+1 = x. ~> range 9007199254740990.0 +inf ▶ (num 9.00719925474099e+15) ▶ (num 9007199254740991.0) ▶ (num 9007199254740992.0) ~> range 9007199254740992.0 9007199254740990.0 ▶ (num 9007199254740992.0) ▶ (num 9007199254740991.0) // invalid step ~> range &step=-0.5 10 Exception: bad value: step must be positive, but is -0.5 [tty]:1:1-19: range &step=-0.5 10 ~> range &step=0.5 10 0 Exception: bad value: step must be negative, but is 0.5 [tty]:1:1-20: range &step=0.5 10 0 // bubbling output error ~> range 1.2 >&- Exception: port does not support value output [tty]:1:1-13: range 1.2 >&- elvish-0.21.0/pkg/eval/builtin_fn_pred.d.elv000066400000000000000000000114301465720375400207050ustar00rootroot00000000000000# Convert a value to boolean. In Elvish, only `$false`, `$nil`, 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 # ``` # # See also [`not`](). fn bool {|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. # # See also [`bool`](). fn not {|value| } # 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 # ``` # # See also [`eq`](). # # Etymology: [Python](https://docs.python.org/3/reference/expressions.html#is). fn is {|@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 # ``` # # See also [`is`]() and [`not-eq`](). # # Etymology: [Perl](https://perldoc.perl.org/perlop.html#Equality-Operators). fn eq {|@values| } # Determines whether `$a` and `$b` are not equal. Equivalent to `not (eq $a $b)`. # # ```elvish-transcript # ~> not-eq 1 2 # ▶ $true # ~> not-eq 1 1 # ▶ $false # ``` # # See also [`eq`](). fn not-eq {|a b| } # Outputs the number -1 if `$a` is smaller than `$b`, 0 if `$a` is equal to # `$b`, and 1 if `$a` is greater than `$b`. # # The following algorithm is used: # # 1. If `$a` and `$b` have the same type and that type is listed below, they are # compared accordingly: # # - Booleans: `$false` is smaller than `$true`. # # - Typed numbers: Compared numerically, consistent with [`<`](#num-lt) # and [`<=`](#num-le), except that `NaN` values are considered equal to # each other and smaller than all other numbers. # # - Strings: Compared lexicographically by bytes, consistent with # [` compare a b # ▶ (num -1) # ~> compare b a # ▶ (num 1) # ~> compare x x # ▶ (num 0) # ~> compare (num 10) (num 1) # ▶ (num 1) # ``` # # Examples comparing values of different types: # # ```elvish-transcript # //skip-test # ~> compare a (num 10) # Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values # [tty]:1:1-18: compare a (num 10) # ~> compare &total a (num 10) # ▶ (num -1) # ~> compare &total (num 10) a # ▶ (num 1) # ``` # # See also [`order`](). fn compare {|&total=$false a b| } elvish-0.21.0/pkg/eval/builtin_fn_pred.go000066400000000000000000000024551465720375400203110ustar00rootroot00000000000000package eval import ( "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Basic predicate commands. func init() { addBuiltinFns(map[string]any{ "bool": vals.Bool, "not": not, "is": is, "eq": eq, "not-eq": notEq, "compare": compare, }) } func not(v any) bool { return !vals.Bool(v) } func is(args ...any) bool { for i := 0; i+1 < len(args); i++ { if args[i] != args[i+1] { return false } } return true } func eq(args ...any) bool { for i := 0; i+1 < len(args); i++ { if !vals.Equal(args[i], args[i+1]) { return false } } return true } func notEq(a, b any) bool { return !vals.Equal(a, b) } // 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"} type compareOptions struct { Total bool } func (opts *compareOptions) SetDefaultOptions() {} func compare(opts compareOptions, a, b any) (int, error) { var o vals.Ordering if opts.Total { o = vals.CmpTotal(a, b) } else { o = vals.Cmp(a, b) } switch o { case vals.CmpLess: return -1, nil case vals.CmpEqual: return 0, nil case vals.CmpMore: return 1, nil default: return 0, ErrUncomparable } } elvish-0.21.0/pkg/eval/builtin_fn_pred_test.elvts000066400000000000000000000066351465720375400221040ustar00rootroot00000000000000//////// # bool # //////// ~> bool $true ▶ $true ~> bool a ▶ $true ~> bool [a] ▶ $true // "empty" values are also true in Elvish ~> bool [] ▶ $true ~> bool [&] ▶ $true ~> bool (num 0) ▶ $true ~> bool "" ▶ $true // only errors, $nil and $false are false ~> bool ?(fail x) ▶ $false ~> bool $nil ▶ $false ~> bool $false ▶ $false /////// # not # /////// ~> not $false ▶ $true ~> not $nil ▶ $true ~> not ?(fail x) ▶ $true ~> not $true ▶ $false ~> not a ▶ $false ////// # is # ////// // The semantics of "is" is not well-defined, so these results might change in // future. ~> is 1 1 ▶ $true ~> is a b ▶ $false ~> is [] [] ▶ $true ~> is [1] [1] ▶ $false ////// # eq # ////// ~> eq 1 1 ▶ $true ~> eq a b ▶ $false ~> eq [] [] ▶ $true ~> eq [1] [1] ▶ $true ~> eq 1 1 2 ▶ $false ////////// # not-eq # ////////// ~> not-eq a b ▶ $true ~> not-eq a a ▶ $false // not-eq only accepts two arguments ~> not-eq Exception: arity mismatch: arguments must be 2 values, but is 0 values [tty]:1:1-6: not-eq ~> not-eq 1 Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-8: not-eq 1 ~> not-eq 1 2 1 Exception: arity mismatch: arguments must be 2 values, but is 3 values [tty]:1:1-12: not-eq 1 2 1 /////////// # compare # /////////// ## strings ## ~> compare a b ▶ (num -1) ~> compare b a ▶ (num 1) ~> compare x x ▶ (num 0) ## numbers ## ~> compare (num 1) (num 2) ▶ (num -1) ~> compare (num 2) (num 1) ▶ (num 1) ~> compare (num 3) (num 3) ▶ (num 0) ~> compare (num 1/4) (num 1/2) ▶ (num -1) ~> compare (num 1/3) (num 0.2) ▶ (num 1) ~> compare (num 3.0) (num 3) ▶ (num 0) ~> compare (num nan) (num 3) ▶ (num -1) ~> compare (num 3) (num nan) ▶ (num 1) ~> compare (num nan) (num nan) ▶ (num 0) ## booleans ## ~> compare $true $false ▶ (num 1) ~> compare $false $true ▶ (num -1) ~> compare $false $false ▶ (num 0) ~> compare $true $true ▶ (num 0) ## lists ## ~> compare [a, b] [a, a] ▶ (num 1) ~> compare [a, a] [a, b] ▶ (num -1) ~> compare [x, y] [x, y] ▶ (num 0) ## different types are uncomparable without &total. ## ~> compare 1 (num 1) Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:1-17: compare 1 (num 1) ~> compare x [x] Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:1-13: compare x [x] ~> compare a [&a=x] Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:1-16: compare a [&a=x] ## uncomparable types ## ~> compare { nop 1 } { nop 2} Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:1-26: compare { nop 1 } { nop 2} ~> compare [&foo=bar] [&a=b] Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:1-25: compare [&foo=bar] [&a=b] ## total ordering for the same comparable type ## ~> compare &total (num 1) (num 3/2) ▶ (num -1) ~> compare &total (num 3/2) (num 2) ▶ (num -1) ## total ordering for the same uncomparable type ## ~> compare &total { nop 1 } { nop 2 } ▶ (num 0) ~> compare &total [&foo=bar] [&a=b] ▶ (num 0) ## total ordering for different types ## ~> == (compare &total foo (num 2)) (compare &total bar (num 10)) ▶ $true ~> + (compare &total foo (num 2)) (compare &total (num 2) foo) ▶ (num 0) elvish-0.21.0/pkg/eval/builtin_fn_str.d.elv000066400000000000000000000032271465720375400205700ustar00rootroot00000000000000#doc:html-id str-lt # Outputs whether `$string`s in the given order are strictly increasing. Outputs # `$true` when given fewer than two strings. fn 's' {|@string| } #doc:html-id str-ge # Outputs whether `$string`s in the given order are strictly non-increasing. # Outputs `$true` when given fewer than two strings. fn '>=s' {|@string| } # Output the width of `$string` when displayed on the terminal. Examples: # # ```elvish-transcript # ~> wcswidth a # ▶ (num 1) # ~> wcswidth lorem # ▶ (num 5) # ~> wcswidth 你好,世界 # ▶ (num 10) # ``` fn wcswidth {|string| } # Convert arguments to string values. # # ```elvish-transcript # ~> to-string foo [a] [&k=v] # ▶ foo # ▶ '[a]' # ▶ '[&k=v]' # ``` fn to-string {|@value| } # 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 # ``` fn base {|base @number| } elvish-0.21.0/pkg/eval/builtin_fn_str.go000066400000000000000000000042551465720375400201670ustar00rootroot00000000000000package eval import ( "fmt" "math" "math/big" "strconv" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/wcwidth" ) // String operations. // TODO(xiaq): Document -override-wcswidth. func init() { addBuiltinFns(map[string]any{ "s": chainStringComparer(func(a, b string) bool { return a > b }), ">=s": chainStringComparer(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, }) } func chainStringComparer(p func(a, b string) bool) func(...string) bool { return func(s ...string) bool { for i := 0; i < len(s)-1; i++ { if !p(s[i], s[i+1]) { return false } } return true } } func toString(fm *Frame, args ...any) error { out := fm.ValueOutput() for _, a := range args { err := out.Put(vals.ToString(a)) if err != nil { return err } } return nil } func base(fm *Frame, b int, nums ...vals.Num) error { if b < 2 || b > 36 { return errs.OutOfRange{What: "base", ValidLow: "2", ValidHigh: "36", Actual: strconv.Itoa(b)} } // Don't output anything yet in case some arguments are invalid. results := make([]string, len(nums)) for i, num := range nums { switch num := num.(type) { case int: results[i] = strconv.FormatInt(int64(num), b) case *big.Int: results[i] = num.Text(b) case float64: if i64 := int64(num); float64(i64) == num { results[i] = strconv.FormatInt(i64, b) } else if num == math.Trunc(num) { var z big.Int z.SetString(fmt.Sprintf("%.0f", num), 10) results[i] = z.Text(b) } else { return errs.BadValue{What: "number", Valid: "integer", Actual: vals.ReprPlain(num)} } default: return errs.BadValue{What: "number", Valid: "integer", Actual: vals.ReprPlain(num)} } } out := fm.ValueOutput() for _, s := range results { err := out.Put(s) if err != nil { return err } } return nil } elvish-0.21.0/pkg/eval/builtin_fn_str_test.elvts000066400000000000000000000037751465720375400217640ustar00rootroot00000000000000///////////////////// # string comparison # ///////////////////// ~> <=s a a ▶ $true ~> <=s a b ▶ $true ~> <=s b a ▶ $false ~> <=s a a b ▶ $true ~> ==s haha haha ▶ $true ~> ==s 10 10.0 ▶ $false ~> ==s a a a ▶ $true ~> >s a b ▶ $false ~> >s 2 10 ▶ $true ~> >s c b a ▶ $true ~> >=s a a ▶ $true ~> >=s a b ▶ $false ~> >=s b a ▶ $true ~> >=s b a a ▶ $true ~> !=s haha haha ▶ $false ~> !=s 10 10.1 ▶ $true // !=s only accepts two arguments ~> !=s a b a Exception: arity mismatch: arguments must be 2 values, but is 3 values [tty]:1:1-9: !=s a b a ///////////// # to-string # ///////////// ~> to-string str (num 1) $true ▶ str ▶ 1 ▶ '$true' // bubbling output errors ~> to-string str >&- Exception: port does not support value output [tty]:1:1-17: to-string str >&- //////// # base # //////// ~> base 2 1 3 4 16 255 ▶ 1 ▶ 11 ▶ 100 ▶ 10000 ▶ 11111111 ~> base 16 42 233 ▶ 2a ▶ e9 // *big.Int ~> base 16 100000000000000000000 ▶ 56bc75e2d63100000 ~> base 10 0x56bc75e2d63100000 ▶ 100000000000000000000 // float64 storing an integer ~> base 16 256.0 ▶ 100 // float64 storing an integer that doesn't fit in int64 ~> base 16 100000000000000000000.0 ▶ 56bc75e2d63100000 // typed number as arguments ~> base (num 16) (num 256) ▶ 100 // bad arguments ~> base 16 1.2 Exception: bad value: number must be integer, but is (num 1.2) [tty]:1:1-11: base 16 1.2 ~> base 8 1/8 Exception: bad value: number must be integer, but is (num 1/8) [tty]:1:1-10: base 8 1/8 ~> base 1 1 Exception: out of range: base must be from 2 to 36, but is 1 [tty]:1:1-8: base 1 1 ~> base 37 10 Exception: out of range: base must be from 2 to 36, but is 37 [tty]:1:1-10: base 37 10 // bubbling output error ~> base 2 1 >&- Exception: port does not support value output [tty]:1:1-12: base 2 1 >&- //////////// # wcswidth # //////////// ~> wcswidth 你好 ▶ (num 4) ~> -override-wcwidth x 10; wcswidth 1x2x; -override-wcwidth x 1 ▶ (num 22) elvish-0.21.0/pkg/eval/builtin_fn_stream.d.elv000066400000000000000000000177631465720375400212650ustar00rootroot00000000000000# Takes [value inputs](#value-inputs), and outputs those values unchanged. # # This is an [identity # function](https://en.wikipedia.org/wiki/Identity_function) for the value # channel; in other words, `a | all` is equivalent to just `a` if `a` only has # value output. # # This command can be used inside output capture (i.e. `(all)`) to turn value # inputs into arguments. For example: # # ```elvish-transcript # ~> echo '["foo","bar"] ["lorem","ipsum"]' | from-json # ▶ [foo bar] # ▶ [lorem ipsum] # ~> echo '["foo","bar"] ["lorem","ipsum"]' | from-json | put (all)[0] # ▶ foo # ▶ lorem # ``` # # The latter pipeline is equivalent to the following: # # ```elvish-transcript # ~> put (echo '["foo","bar"] ["lorem","ipsum"]' | from-json)[0] # ▶ foo # ▶ lorem # ``` # # In general, when `(all)` appears in the last command of a pipeline, it is # equivalent to just moving the previous commands of the pipeline into `()`. # The choice is a stylistic one; the `(all)` variant is longer overall, but can # be more readable since it allows you to avoid putting an excessively long # pipeline inside an output capture, and keeps the data flow within the # pipeline. # # Putting the value capture inside `[]` (i.e. `[(all)]`) is useful for storing # all value inputs in a list for further processing: # # ```elvish-transcript # ~> fn f { var inputs = [(all)]; put $inputs[1] } # ~> put foo bar baz | f # ▶ bar # ``` # # The `all` command can also take "inputs" from an iterable argument. This can # be used to flatten lists or strings (although not recursively): # # ```elvish-transcript # ~> all [foo [lorem ipsum]] # ▶ foo # ▶ [lorem ipsum] # ~> all foo # ▶ f # ▶ o # ▶ o # ``` # # This can be used together with `(one)` to turn a single iterable value in the # pipeline into its elements: # # ```elvish-transcript # ~> echo '["foo","bar","lorem"]' | from-json # ▶ [foo bar lorem] # ~> echo '["foo","bar","lorem"]' | from-json | all (one) # ▶ foo # ▶ bar # ▶ lorem # ``` # # When given byte inputs, the `all` command currently functions like # [`from-lines`](), although this behavior is subject to change: # # ```elvish-transcript # ~> print "foo\nbar\n" | all # ▶ foo # ▶ bar # ``` # # See also [`one`](). fn all {|inputs?| } # Takes exactly one [value input](#value-inputs) and outputs it. If there are # more than one value inputs, raises an exception. # # This function can be used in a similar way to [`all`](), but is a better # choice when you expect that there is exactly one output. # # See also [`all`](). fn one {|inputs?| } # Outputs the first `$n` [value inputs](#value-inputs). If `$n` is larger than # the number of value inputs, outputs everything. # # Examples: # # ```elvish-transcript # ~> range 2 | take 10 # ▶ (num 0) # ▶ (num 1) # ~> take 3 [a b c d e] # ▶ a # ▶ b # ▶ c # ~> use str # ~> str:split ' ' 'how are you?' | take 1 # ▶ how # ``` # # Etymology: Haskell. # # See also [`drop`](). fn take {|n inputs?| } # Ignores the first `$n` [value inputs](#value-inputs) and outputs the rest. # If `$n` is larger than the number of value inputs, outputs nothing. # # Example: # # ```elvish-transcript # ~> range 10 | drop 8 # ▶ (num 8) # ▶ (num 9) # ~> range 2 | drop 10 # ~> drop 2 [a b c d e] # ▶ c # ▶ d # ▶ e # ~> use str # ~> str:split ' ' 'how are you?' | drop 1 # ▶ are # ▶ 'you?' # ``` # # Etymology: Haskell. # # See also [`take`](). fn drop {|n inputs?| } # Replaces consecutive runs of equal values with a single copy. Similar to the # `uniq` command on Unix. # # Examples: # # ```elvish-transcript # ~> put a a b b c | compact # ▶ a # ▶ b # ▶ c # ~> compact [a a b b c] # ▶ a # ▶ b # ▶ c # ~> put a b a | compact # ▶ a # ▶ b # ▶ a # ``` fn compact {|inputs?| } # Count the number of elements in a container (also known as its _length_), or # the number of inputs when when argument is given. # # Examples: # # ```elvish-transcript # ~> count [lorem ipsum] # ▶ (num 2) # ~> count [&foo=bar &lorem=ipsum] # count pairs in a map # ▶ (num 2) # ~> count lorem # count bytes in a string # ▶ (num 5) # ~> range 100 | count # count inputs # ▶ (num 100) # ~> seq 100 | count # ▶ (num 100) # ``` fn count {|inputs?| } # Outputs the [value inputs](#value-inputs) after sorting. The sorting process # is # [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). # # By default, `order` sorts the values in ascending order, using the same # comparator as [`compare`](), which only supports values of the same ordered # type. Its options modify this behavior: # # - The `&less-than` option, if given, overrides the comparator. Its # value should be a function that takes two arguments `$a` and `$b` and # outputs a boolean indicating whether `$a` is less than `$b`. If the # function throws an exception, `order` rethrows the exception without # outputting any value. # # The default behavior of `order` is equivalent to `order &less-than={|a b| # == -1 (compare $a $b)}`. # # - The `&total` option, if true, overrides the comparator to be same as # `compare &total=$true`, which allows sorting values of mixed types and # unordered types. The result groups values by their types. Groups of # ordered types are sorted internally, and groups of unordered types retain # their original relative order. # # Specifying `&total=$true` is equivalent to specifying `&less-than={|a b| # == -1 (compare &total=$true $a $b)}`. It is an error to both specify # `&total=$true` and a non-nil `&less-than` callback. # # - The `&key` option, if given, is a function that gets called with each # input value. It must output a single value, which is used for comparison # in place of the original value. The comparator used can be affected by # `$less-than` or `&total`. # # If the function throws an exception, `order` rethrows the exception. # # Use of `&key` can usually be rewritten to use `&less-than` instead, but # using `&key` can be faster. The `&key` callback is only called once for # each element, whereas the `&less-than` callback is called O(n*lg(n)) times # on average. # # - The `&reverse` option, if true, reverses the order of output. # # Examples: # # ```elvish-transcript # ~> put foo bar ipsum | order # ▶ bar # ▶ foo # ▶ ipsum # ~> order [(num 10) (num 1) (num 5)] # ▶ (num 1) # ▶ (num 5) # ▶ (num 10) # ~> order [[a b] [a] [b b] [a c]] # ▶ [a] # ▶ [a b] # ▶ [a c] # ▶ [b b] # ~> order &reverse [a c b] # ▶ c # ▶ b # ▶ a # ~> put [0 x] [1 a] [2 b] | order &key={|l| put $l[1]} # ▶ [1 a] # ▶ [2 b] # ▶ [0 x] # ~> order &less-than={|a b| eq $a x } [l x o r x e x m] # ▶ x # ▶ x # ▶ x # ▶ l # ▶ o # ▶ r # ▶ e # ▶ m # ``` # # Ordering heterogeneous values: # # ```elvish-transcript # //skip-test # ~> order [a (num 2) c (num 0) b (num 1)] # Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values # [tty]:1:1-37: order [a (num 2) c (num 0) b (num 1)] # ~> order &total [a (num 2) c (num 0) b (num 1)] # ▶ (num 0) # ▶ (num 1) # ▶ (num 2) # ▶ a # ▶ b # ▶ c # ``` # # Beware that strings that look like numbers are treated as strings, not # numbers. To sort strings as numbers, use an explicit `&key` or `&less-than`: # # ```elvish-transcript # ~> order [5 1 10] # ▶ 1 # ▶ 10 # ▶ 5 # ~> order &key=$num~ [5 1 10] # ▶ 1 # ▶ 5 # ▶ 10 # ~> order &less-than=$"<~" [5 1 10] # ▶ 1 # ▶ 5 # ▶ 10 # ``` # # (The `$"<~"` syntax is a reference to [the `<` function](#num-lt).) # # See also [`compare`](). fn order {|&less-than=$nil &total=$false &key=$nil &reverse=$false inputs?| } #doc:added-in 0.21 # Calls `$predicate` for each input, outputting those where `$predicate` outputs # `$true`. Similar to `filter` in some other languages. # # The `$predicate` must output a single boolean value. # # Examples: # # ```elvish-transcript # ~> use str # ~> put foo bar foobar | keep-if {|s| str:has-prefix $s f } # ▶ foo # ▶ foobar # ~> keep-if {|s| == 3 (count $s) } [foo bar foobar] # ▶ foo # ▶ bar # ``` fn keep-if {|predicate inputs?| } elvish-0.21.0/pkg/eval/builtin_fn_stream.go000066400000000000000000000132501465720375400206450ustar00rootroot00000000000000package eval import ( "errors" "fmt" "sort" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Stream manipulation. func init() { addBuiltinFns(map[string]any{ "all": all, "one": one, "take": take, "drop": drop, "compact": compact, "count": count, "order": order, // Iterations "keep-if": keepIf, }) } func all(fm *Frame, inputs Inputs) error { out := fm.ValueOutput() var errOut error inputs(func(v any) { if errOut != nil { return } errOut = out.Put(v) }) return errOut } func one(fm *Frame, inputs Inputs) error { var val any n := 0 inputs(func(v any) { if n == 0 { val = v } n++ }) if n == 1 { return fm.ValueOutput().Put(val) } return errs.ArityMismatch{What: "values", ValidLow: 1, ValidHigh: 1, Actual: n} } func take(fm *Frame, n int, inputs Inputs) error { out := fm.ValueOutput() var errOut error i := 0 inputs(func(v any) { if errOut != nil { return } if i < n { errOut = out.Put(v) } i++ }) return errOut } func drop(fm *Frame, n int, inputs Inputs) error { out := fm.ValueOutput() var errOut error i := 0 inputs(func(v any) { if errOut != nil { return } if i >= n { errOut = out.Put(v) } i++ }) return errOut } func compact(fm *Frame, inputs Inputs) error { out := fm.ValueOutput() first := true var errOut error var prev any inputs(func(v any) { if errOut != nil { return } if first || !vals.Equal(v, prev) { errOut = out.Put(v) first = false prev = v } }) return errOut } // 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 ...any) (int, error) { var n int switch nargs := len(args); nargs { case 0: // Count inputs. fm.IterateInputs(func(any) { 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(any) 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 } type orderOptions struct { Reverse bool Key Callable Total bool LessThan Callable } func (opt *orderOptions) SetDefaultOptions() {} // ErrBothTotalAndLessThan is returned by order when both the &total and // &less-than options are specified. var ErrBothTotalAndLessThan = errors.New("both &total and &less-than specified") func order(fm *Frame, opts orderOptions, inputs Inputs) error { if opts.Total && opts.LessThan != nil { return ErrBothTotalAndLessThan } var values, keys []any inputs(func(v any) { values = append(values, v) }) if opts.Key != nil { keys = make([]any, len(values)) for i, value := range values { outputs, err := fm.CaptureOutput(func(fm *Frame) error { return opts.Key.Call(fm, []any{value}, NoOpts) }) if err != nil { return err } else if len(outputs) != 1 { return errs.ArityMismatch{ What: "number of outputs of the &key callback", ValidLow: 1, ValidHigh: 1, Actual: len(outputs)} } keys[i] = outputs[0] } } s := &slice{fm, opts.Total, opts.LessThan, values, keys, nil} if opts.Reverse { sort.Stable(sort.Reverse(s)) } else { sort.Stable(s) } if s.err != nil { return s.err } out := fm.ValueOutput() for _, v := range values { err := out.Put(v) if err != nil { return err } } return nil } type slice struct { fm *Frame total bool lessThan Callable values []any keys []any // nil if no keys err error } func (s *slice) Len() int { return len(s.values) } func (s *slice) Less(i, j int) bool { if s.err != nil { return true } var a, b any if s.keys != nil { a, b = s.keys[i], s.keys[j] } else { a, b = s.values[i], s.values[j] } if s.lessThan == nil { // Use a builtin comparator depending on s.mixed. if s.total { return vals.CmpTotal(a, b) == vals.CmpLess } o := vals.Cmp(a, b) if o == vals.CmpUncomparable { s.err = ErrUncomparable return true } return o == vals.CmpLess } // Use the &less-than callback. outputs, err := s.fm.CaptureOutput(func(fm *Frame) error { return s.lessThan.Call(fm, []any{a, b}, NoOpts) }) if err != nil { s.err = err return true } if len(outputs) != 1 { s.err = errs.ArityMismatch{ What: "number of outputs of the &less-than callback", ValidLow: 1, ValidHigh: 1, Actual: len(outputs)} return true } if b, ok := outputs[0].(bool); ok { return b } s.err = errs.BadValue{ What: "output of the &less-than callback", Valid: "boolean", Actual: vals.Kind(outputs[0])} return true } func (s *slice) Swap(i, j int) { s.values[i], s.values[j] = s.values[j], s.values[i] if s.keys != nil { s.keys[i], s.keys[j] = s.keys[j], s.keys[i] } } func keepIf(fm *Frame, f Callable, inputs Inputs) error { var err error inputs(func(v any) { if err != nil { return } outputs, errF := fm.CaptureOutput(func(fm *Frame) error { return f.Call(fm, []any{v}, NoOpts) }) if errF != nil { err = errF } else if len(outputs) != 1 { err = errs.ArityMismatch{ What: "number of callback outputs", ValidLow: 1, ValidHigh: 1, Actual: len(outputs), } } else { b, ok := outputs[0].(bool) if !ok { err = errs.BadValue{What: "callback output", Valid: "bool", Actual: vals.ReprPlain(outputs[0])} } else if b { err = fm.ValueOutput().Put(v) } } }) return err } elvish-0.21.0/pkg/eval/builtin_fn_stream_test.elvts000066400000000000000000000172161465720375400224420ustar00rootroot00000000000000/////// # all # /////// ~> put foo bar | all ▶ foo ▶ bar ~> echo foobar | all ▶ foobar ~> all [foo bar] ▶ foo ▶ bar // bubbling output errors ~> all [foo bar] >&- Exception: port does not support value output [tty]:1:1-17: all [foo bar] >&- /////// # one # /////// ~> put foo | one ▶ foo ~> put | one Exception: arity mismatch: values must be 1 value, but is 0 values [tty]:1:7-9: put | one ~> put foo bar | one Exception: arity mismatch: values must be 1 value, but is 2 values [tty]:1:15-17: put foo bar | one ~> one [foo] ▶ foo ~> one [] Exception: arity mismatch: values must be 1 value, but is 0 values [tty]:1:1-6: one [] ~> one [foo bar] Exception: arity mismatch: values must be 1 value, but is 2 values [tty]:1:1-13: one [foo bar] // bubbling output errors ~> one [foo] >&- Exception: port does not support value output [tty]:1:1-13: one [foo] >&- //////// # take # //////// ~> range 100 | take 2 ▶ (num 0) ▶ (num 1) // bubbling output errors ~> take 1 [foo bar] >&- Exception: port does not support value output [tty]:1:1-20: take 1 [foo bar] >&- //////// # drop # //////// ~> range 100 | drop 98 ▶ (num 98) ▶ (num 99) // bubbling output errors ~> drop 1 [foo bar lorem] >&- Exception: port does not support value output [tty]:1:1-26: drop 1 [foo bar lorem] >&- /////////// # compact # /////////// ~> put a a b b c | compact ▶ a ▶ b ▶ c ~> put a b a | compact ▶ a ▶ b ▶ a // bubbling output errors ~> compact [a a] >&- Exception: port does not support value output [tty]:1:1-17: compact [a a] >&- ///////// # count # ///////// ~> range 100 | count ▶ (num 100) ~> count [(range 100)] ▶ (num 100) ~> count 123 ▶ (num 3) ~> count 1 2 3 Exception: arity mismatch: arguments must be 0 to 1 values, but is 3 values [tty]:1:1-11: count 1 2 3 ~> count $true Exception: cannot get length of a bool [tty]:1:1-11: count $true ///////// # order # ///////// ## strings ## ~> put foo bar ipsum | order ▶ bar ▶ foo ▶ ipsum ~> put foo bar bar | order ▶ bar ▶ bar ▶ foo ~> put 10 1 5 2 | order ▶ 1 ▶ 10 ▶ 2 ▶ 5 ## booleans ## ~> put $true $false $true | order ▶ $false ▶ $true ▶ $true ~> put $false $true $false | order ▶ $false ▶ $false ▶ $true ## typed numbers ## // int ~> put 10 1 1 | each $num~ | order ▶ (num 1) ▶ (num 1) ▶ (num 10) ~> put 10 1 5 2 -1 | each $num~ | order ▶ (num -1) ▶ (num 1) ▶ (num 2) ▶ (num 5) ▶ (num 10) // int and *big.Int ~> put 1 100000000000000000000 2 100000000000000000000 | each $num~ | order ▶ (num 1) ▶ (num 2) ▶ (num 100000000000000000000) ▶ (num 100000000000000000000) // int and *big.Rat ~> put 1 2 3/2 3/2 | each $num~ | order ▶ (num 1) ▶ (num 3/2) ▶ (num 3/2) ▶ (num 2) // int and float64 ~> put 1 1.5 2 1.5 | each $num~ | order ▶ (num 1) ▶ (num 1.5) ▶ (num 1.5) ▶ (num 2) // NaN's are considered smaller than other numbers for ordering ~> put NaN -1 NaN | each $num~ | order ▶ (num NaN) ▶ (num NaN) ▶ (num -1) ## lists ## ~> put [b] [a] | order ▶ [a] ▶ [b] ~> put [a] [b] [a] | order ▶ [a] ▶ [a] ▶ [b] ~> put [(num 10)] [(num 2)] | order ▶ [(num 2)] ▶ [(num 10)] ~> put [a b] [b b] [a c] | order ▶ [a b] ▶ [a c] ▶ [b b] ~> put [a] [] [a (num 2)] [a (num 1)] | order ▶ [] ▶ [a] ▶ [a (num 1)] ▶ [a (num 2)] ## &reverse ## ~> put foo bar ipsum | order &reverse ▶ ipsum ▶ foo ▶ bar ## &key ## ~> put 10 1 5 2 | order &key={|v| num $v } ▶ 1 ▶ 2 ▶ 5 ▶ 10 ## &key and &reverse ## ~> put 10 1 5 2 | order &reverse &key={|v| num $v } ▶ 10 ▶ 5 ▶ 2 ▶ 1 ## different types without &total ## ~> put (num 1) 1 | order Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:17-21: put (num 1) 1 | order ~> put 1 (num 1) | order Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:17-21: put 1 (num 1) | order ~> put 1 (num 1) b | order Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:19-23: put 1 (num 1) b | order ~> put [a] a | order Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:13-17: put [a] a | order ~> put [a] [(num 1)] | order Exception: bad value: inputs to "compare" or "order" must be comparable values, but is uncomparable values [tty]:1:21-25: put [a] [(num 1)] | order ## different types with &total ## // &total orders the values into groups of different types, and sorts // the groups themselves. Test that without assuming the relative order // between numbers and strings. ~> put (num 3/2) (num 1) c (num 2) a | order &total | var li = [(all)] or (eq $li [a c (num 1) (num 3/2) (num 2)]) ^ (eq $li [(num 1) (num 3/2) (num 2) a c]) ▶ $true // &total keeps the order of unordered values as is. ~> put [&foo=bar] [&a=b] [&x=y] | order &total ▶ [&foo=bar] ▶ [&a=b] ▶ [&x=y] ## &less-than ## ~> put 1 10 2 5 | order &less-than={|a b| < $a $b } ▶ 1 ▶ 2 ▶ 5 ▶ 10 ## &less-than and &key ## ~> put [a 1] [b 10] [c 2] | order &key={|v| put $v[1]} &less-than=$'<~' ▶ [a 1] ▶ [c 2] ▶ [b 10] ## &less-than and &reverse ## ~> put 1 10 2 5 | order &reverse &less-than={|a b| < $a $b } ▶ 10 ▶ 5 ▶ 2 ▶ 1 ## &less-than writing more than one value ## ~> put 1 10 2 5 | order &less-than={|a b| put $true $false } Exception: arity mismatch: number of outputs of the &less-than callback must be 1 value, but is 2 values [tty]:1:16-57: put 1 10 2 5 | order &less-than={|a b| put $true $false } ## &less-than writing non-boolean value ## ~> put 1 10 2 5 | order &less-than={|a b| put x } Exception: bad value: output of the &less-than callback must be boolean, but is string [tty]:1:16-46: put 1 10 2 5 | order &less-than={|a b| put x } ## &less-than throwing an exception ## ~> put 1 10 2 5 | order &less-than={|a b| fail bad } Exception: bad [tty]:1:40-48: put 1 10 2 5 | order &less-than={|a b| fail bad } [tty]:1:16-49: put 1 10 2 5 | order &less-than={|a b| fail bad } ## all callback options support $nil for default behavior ## ~> put c b a | order &less-than=$nil &key=$nil ▶ a ▶ b ▶ c ## sort is stable ## // Test stability by pretending that all values but one are equal, and check // that the order among them has not changed. ~> put l x o x r x e x m | order &less-than={|a b| eq $a x } ▶ x ▶ x ▶ x ▶ x ▶ l ▶ o ▶ r ▶ e ▶ m ## &total and &less-than are mutually exclusive ## ~> put x | order &total &less-than={|a b| put $true } Exception: both &total and &less-than specified [tty]:1:9-50: put x | order &total &less-than={|a b| put $true } ## bubbling output errors ## ~> order [foo] >&- Exception: port does not support value output [tty]:1:1-15: order [foo] >&- /////////// # keep-if # /////////// ~> use str ~> put foo bar foobar | keep-if {|s| str:has-prefix $s f} ▶ foo ▶ foobar ## wrong number of outputs ## ~> put foo bar foobar | keep-if {|_| } Exception: arity mismatch: number of callback outputs must be 1 value, but is 0 values [tty]:1:22-35: put foo bar foobar | keep-if {|_| } ~> put foo bar foobar | keep-if {|_| put $true $false } Exception: arity mismatch: number of callback outputs must be 1 value, but is 2 values [tty]:1:22-52: put foo bar foobar | keep-if {|_| put $true $false } ## wrong type of output ## ~> put foo bar foobar | keep-if {|_| put foo } Exception: bad value: callback output must be bool, but is foo [tty]:1:22-43: put foo bar foobar | keep-if {|_| put foo } ## callback throws exception ## ~> put foo bar foobar | keep-if {|_| fail bad} Exception: bad [tty]:1:35-42: put foo bar foobar | keep-if {|_| fail bad} [tty]:1:22-43: put foo bar foobar | keep-if {|_| fail bad} elvish-0.21.0/pkg/eval/builtin_fn_styled.d.elv000066400000000000000000000117711465720375400212670ustar00rootroot00000000000000# Constructs a styled segment, a building block for styled texts. # # - If `$object` is a string, constructs a styled segment with `$object` as the # content, and the properties specified by the options. # # - If `$object` is a styled segment, constructs a styled segment that is a # copy of `$object`, with the properties specified by the options overridden. # # The properties of styled segments can be inspected by indexing into it. Valid # keys are the same as the options to `styled-segment`, plus `text` for the # string content: # # ```elvish-transcript # ~> var s = (styled-segment abc &bold) # ~> put $s[text] # ▶ abc # ~> put $s[fg-color] # ▶ default # ~> put $s[bold] # ▶ $true # ``` # # Prefer the high-level [`styled`]() command to build and transform styled # texts. Styled segments are a low-level construct, and you only have to deal # with it when building custom style transformers. # # In the following example, a custom transformer sets the `inverse` property # for every bold segment: # # ```elvish # styled foo(styled bar bold) {|x| styled-segment $x &inverse=$x[bold] } # # transforms "foo" + bold "bar" into "foo" + bold and inverse "bar" # ``` fn styled-segment {|object &fg-color=default &bg-color=default &bold=$false &dim=$false &italic=$false &underlined=$false &blink=$false &inverse=$false| } # Constructs a **styled text** by applying the supplied transformers to the # supplied `$object`, which may be a string, a [styled # segment](#styled-segment), or an existing styled text. # # Each `$style-transformer` can be one of the following: # # - A boolean attribute name: # # - One of `bold`, `dim`, `italic`, `underlined`, `blink` and `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 `#` introduces # a comment (e.g. use `'bg-#778899'` instead of `bg-#778899`). # # - 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 color name prefixed by `bg-` to set the background color. # # - A function that receives a styled segment as the only argument and outputs # a single styled segment: this function will be applied to all the segments. # # 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. # # If the [`NO_COLOR`](https://no-color.org) environment variable is set and # non-empty when Elvish starts, color output is suppressed. Modifications to # `NO_COLOR` within Elvish (including from `rc.elv`) do not affect the current # process, but will affect child Elvish processes. # # Examples: # # ```elvish # echo (styled foo red bold) # prints red bold "foo" # echo (styled (styled foo red bold) green) # prints green bold "foo" # ``` # # A styled text can contain multiple [segments](#styled-segment) with different # styles. Such styled texts can be constructed by concatenating multiple styled # texts with the [compounding](language.html#compounding) syntax. Strings and # styled segments are automatically "promoted" to styled texts when # concatenating. Examples: # # ```elvish # echo foo(styled bar red) # prints "foo" + red "bar" # echo (styled foo bold)(styled bar red) # prints bold "foo" + red "bar" # ``` # # The individual segments in a styled text can be extracted by indexing: # # ```elvish # var s = (styled abc red)(styled def green) # put $s[0] $s[1] # ``` # # When printed to the terminal, a styled text is not affected by any existing # SGR styles in effect, and it will always reset the SGR style afterwards. For # example: # # ```elvish # print "\e[1m" # echo (styled foo red) # echo bar # # "foo" will be printed as red, but not bold # # "bar" will be printed without any style # ``` # # See also [`render-styledown`](). fn styled {|object @style-transformer| } #doc:added-in 0.21 # Renders "styledown" markup into a styled text. For the styledown markup # format, see . # # Examples: # # ```elvish-transcript # ~> render-styledown ' # foo bar # *** ### # '[1..] # ▶ [^styled (styled-segment foo &bold) ' ' (styled-segment bar &inverse) "\n"] # ``` # # To see the rendered text in the terminal, pass it to [`print`](), like # `render-styledown ... | print (one)`. # # See also [`styled`](). fn render-styledown {|s| } elvish-0.21.0/pkg/eval/builtin_fn_styled.go000066400000000000000000000044741465720375400206660ustar00rootroot00000000000000package eval import ( "errors" "fmt" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/ui/styledown" ) var errStyledSegmentArgType = errors.New("argument to styled-segment must be a string or a styled segment") func init() { addBuiltinFns(map[string]any{ "styled-segment": styledSegment, "styled": styled, "render-styledown": styledown.Render, }) } // 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 any) (*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 } func styled(fm *Frame, input any, stylings ...any) (ui.Text, error) { var text ui.Text switch input := input.(type) { case string: text = ui.T(input) case *ui.Segment: text = ui.TextFromSegment(input) 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, []any{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.21.0/pkg/eval/builtin_fn_styled_test.elvts000066400000000000000000000126171465720375400224530ustar00rootroot00000000000000// The transcript testing framework strips SGR sequences from stdout and stderr, // so we need to use to-string when testing them. ////////////////// # styled-segment # ////////////////// ## converting a string to a segment ## ~> to-string (styled-segment abc) ▶ "\e[mabc" ## styling a string ## ~> to-string (styled-segment abc &fg-color=red) ▶ "\e[;31mabc\e[m" ## overriding the style of an existing segment ## ~> to-string (styled-segment (styled-segment abc &fg-color=red) &fg-color=magenta) ▶ "\e[;35mabc\e[m" ## bad usage ## ~> styled-segment [] Exception: argument to styled-segment must be a string or a styled segment [tty]:1:1-17: styled-segment [] ~> styled-segment text &foo=bar Exception: unrecognized option 'foo' [tty]:1:1-28: styled-segment text &foo=bar ## introspection ## ~> put (styled-segment abc &italic=$true &fg-color=red)[bold] ▶ $false ~> put (styled-segment abc &italic=$true &fg-color=red)[italic] ▶ $true ~> put (styled-segment abc &italic=$true &fg-color=red)[fg-color] ▶ red ////////// # styled # ////////// ## converting and transforming strings ## ~> to-string (styled abc) ▶ "\e[mabc" ~> to-string (styled abc bold) ▶ "\e[;1mabc\e[m" ## converting and transforming styled segments ## ~> to-string (styled (styled-segment abc &fg-color=red)) ▶ "\e[;31mabc\e[m" ~> to-string (styled (styled-segment abc &fg-color=red) bold) ▶ "\e[;1;31mabc\e[m" ## transforming another styled text ## ~> to-string (styled (styled abc red) bold) ▶ "\e[;1;31mabc\e[m" ## function as transformer ## ~> to-string (styled abc {|s| put $s }) ▶ "\e[mabc" ~> to-string (styled abc {|s| styled-segment $s &bold=$true &italic=$false }) ▶ "\e[;1mabc\e[m" ## mixed string and function transformers ## ~> to-string (styled abc italic {|s| styled-segment $s &bold=$true }) ▶ "\e[;1;3mabc\e[m" ## error from function transformer ## ~> styled abc {|_| fail bad } Exception: bad [tty]:1:17-25: styled abc {|_| fail bad } [tty]:1:1-26: styled abc {|_| fail bad } ~> styled abc {|_| put a b } Exception: styling function must return a single segment; got 2 values [tty]:1:1-25: styled abc {|_| put a b } ~> styled abc {|_| put [] } Exception: styling function must return a segment; got list [tty]:1:1-24: styled abc {|_| put [] } ## bad usage ## ~> styled abc hopefully-never-exists Exception: hopefully-never-exists is not a valid style transformer [tty]:1:1-33: styled abc hopefully-never-exists ~> styled [] Exception: expected string, styled segment or styled text; got list [tty]:1:1-9: styled [] ~> styled abc [] Exception: need string or callable; got list [tty]:1:1-13: styled abc [] ## doesn't modify the argument ## ~> var x = (styled text) var y = (styled $x red) put $x[0][fg-color] ▶ default ~> var x = (styled-segment text) var y = (styled $x red) put $x[fg-color] ▶ default ## introspection ## ~> put (styled abc red)[0][bold] ▶ $false ~> put (styled abc red)[0][bg-color] ▶ default ///////////////////////////// # concatenating styled text # ///////////////////////////// ## segment + string ## ~> to-string (styled-segment abc &fg-color=red)abc ▶ "\e[;31mabc\e[mabc" ## segment + segment ## ~> to-string (styled-segment abc &bg-color=red)(styled-segment abc &fg-color=red) ▶ "\e[;41mabc\e[;31mabc\e[m" ## segment + text ## ~> to-string (styled-segment abc &underlined=$true)(styled abc bright-cyan) ▶ "\e[;4mabc\e[;96mabc\e[m" ## segment + num ## ~> to-string (styled-segment abc &blink)(num 44/3) ▶ "\e[;5mabc\e[m44/3" ~> to-string (styled-segment abc &blink)(num 42) ▶ "\e[;5mabc\e[m42" ## segment + unsupported ## ~> to-string (styled-segment abc){ } Exception: cannot concatenate ui:text-segment and fn [tty]:1:11-33: to-string (styled-segment abc){ } ## string + segment ## ~> to-string abc(styled-segment abc &fg-color=red) ▶ "\e[mabc\e[31mabc\e[m" ## num + segment ## ~> to-string (num 99.0)(styled-segment abc &blink) ▶ "\e[m99.0\e[5mabc\e[m" ~> to-string (num 66)(styled-segment abc &blink) ▶ "\e[m66\e[5mabc\e[m" ~> to-string (num 3/2)(styled-segment abc &blink) ▶ "\e[m3/2\e[5mabc\e[m" ## unsupported + segment ## ~> to-string { }(styled-segment abc) Exception: cannot concatenate fn and ui:text-segment [tty]:1:11-33: to-string { }(styled-segment abc) ## text + string ## ~> to-string (styled abc blink)abc ▶ "\e[;5mabc\e[mabc" ## text + number ## ~> to-string (styled abc blink)(num 13) ▶ "\e[;5mabc\e[m13" ~> to-string (styled abc blink)(num 3/4) ▶ "\e[;5mabc\e[m3/4" ## text + segment ## ~> to-string (styled abc inverse)(styled-segment abc &bg-color=white) ▶ "\e[;7mabc\e[;47mabc\e[m" ## text + text ## ~> to-string (styled abc bold)(styled abc dim) ▶ "\e[;1mabc\e[;2mabc\e[m" ## text + unsupported ## ~> to-string (styled abc){ } Exception: cannot concatenate ui:text and fn [tty]:1:11-25: to-string (styled abc){ } ## string + text ## ~> to-string abc(styled abc blink) ▶ "\e[mabc\e[5mabc\e[m" ## number + text ## ~> to-string (num 13)(styled abc blink) ▶ "\e[m13\e[5mabc\e[m" ~> to-string (num 4/3)(styled abc blink) ▶ "\e[m4/3\e[5mabc\e[m" ## unsupported + text ## ~> to-string { }(styled abc) Exception: cannot concatenate fn and ui:text [tty]:1:11-25: to-string { }(styled abc) ## introspecting concatenated text ## ~> var t = (styled-segment abc &underlined=$true)(styled abc bright-cyan) put $t[1][fg-color] ▶ bright-cyan ~> var t = (styled-segment abc &underlined=$true)(styled abc bright-cyan) put $t[1][underlined] ▶ $false elvish-0.21.0/pkg/eval/builtin_fn_time.d.elv000066400000000000000000000111011465720375400207040ustar00rootroot00000000000000#//skip-test # 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`](). # # 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 # ``` fn sleep {|duration| } # 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 # ~> var t = '' # ~> time &on-end={|x| set t = $x } { sleep 1 } # ~> put $t # ▶ (num 1.000925004) # ~> time &on-end={|x| set t = $x } { sleep 0.01 } # ~> put $t # ▶ (num 0.011030208) # ``` # # See also [`benchmark`](). fn time {|&on-end=$nil callable| } # Runs `$callable` repeatedly, and reports statistics about how long each run # takes. # # If the `&on-end` callback is not given, `benchmark` prints the average, # standard deviation, minimum and maximum of the time it took to run # `$callback`, and the number of runs. If the `&on-end` callback is given, # `benchmark` instead calls it with a map containing these metrics, keyed by # `avg`, `stddev`, `min`, `max` and `runs`. Each duration value (i.e. all # except `runs`) is given as the number of seconds. # # The number of runs is controlled by `&min-runs` and `&min-time`. The # `$callable` is run at least `&min-runs` times, **and** when the total # duration is at least `&min-time`. # # The `&min-runs` option must be a non-negative integer within the range of the # machine word. # # The `&min-time` option must be a string representing a non-negative duration, # specified as a sequence of decimal numbers with a unit suffix (the numbers # may have fractional parts), such as "300ms", "1.5h" and "1h45m7s". Valid time # units are "ns", "us" (or "µs"), "ms", "s", "m", "h". # # If `&on-run-end` is given, it is called after each call to `$callable`, with # the time that call took, given as the number of seconds. # # If `$callable` throws an exception, `benchmark` terminates and propagates the # exception after the `&on-end` callback (or the default printing behavior) # finishes. The duration of the call that throws an exception is not passed to # `&on-run-end`, nor is it included when calculating the statistics for # `&on-end`. If the first call to `$callable` throws an exception and `&on-end` # is `$nil`, nothing is printed and any `&on-end` callback is not called. # # If `&on-run-end` is given and throws an exception, `benchmark` terminates and # propagates the exception after the `&on-end` callback (or the default # printing behavior) finishes, unless `$callable` has already thrown an # exception # # If `&on-end` throws an exception, the exception is propagated, unless # `$callable` or `&on-run-end` has already thrown an exception. # # Example: # # ```elvish-transcript # ~> benchmark { } # 98ns ± 382ns (min 0s, max 210.417µs, 10119226 runs) # ~> benchmark &on-end={|m| put $m[avg]} { } # ▶ (num 9.8e-08) # ~> benchmark &on-run-end={|d| echo $d} { sleep 0.3 } # 0.301123625 # 0.30123775 # 0.30119075 # 0.300629166 # 0.301260333 # 301.088324ms ± 234.298µs (min 300.629166ms, max 301.260333ms, 5 runs) # ``` # # See also [`time`](). fn benchmark {|&min-runs=5 &min-time=1s &on-end=$nil &on-run-end=$nil callable| } elvish-0.21.0/pkg/eval/builtin_fn_time.go000066400000000000000000000102601465720375400203060ustar00rootroot00000000000000package eval import ( "fmt" "math" "math/big" "strconv" "time" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) func init() { addBuiltinFns(map[string]any{ "sleep": sleep, "time": timeCmd, "benchmark": benchmark, }) } var ( // Reference to [time.After] that can be mutated for testing. Takes an // additional Frame argument to allow inspection of the value of d in tests. timeAfter = func(fm *Frame, d time.Duration) <-chan time.Time { return time.After(d) } // Reference to [time.Now] that can be overridden in tests. timeNow = time.Now ) func sleep(fm *Frame, duration any) 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.Context().Done(): return ErrInterrupted case <-timeAfter(fm, d): return nil } } 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() errCb := opts.OnEnd.Call(newFm, []any{dt.Seconds()}, NoOpts) if err == nil { err = errCb } } else { _, errWrite := fmt.Fprintln(fm.ByteOutput(), dt) if err == nil { err = errWrite } } return err } type benchmarkOpts struct { OnEnd Callable OnRunEnd Callable MinRuns int MinTime string minTime time.Duration } func (o *benchmarkOpts) SetDefaultOptions() { o.MinRuns = 5 o.minTime = time.Second } func (opts *benchmarkOpts) parse() error { if opts.MinRuns < 0 { return errs.BadValue{What: "min-runs option", Valid: "non-negative integer", Actual: strconv.Itoa(opts.MinRuns)} } if opts.MinTime != "" { d, err := time.ParseDuration(opts.MinTime) if err != nil { return errs.BadValue{What: "min-time option", Valid: "duration string", Actual: parse.Quote(opts.MinTime)} } if d < 0 { return errs.BadValue{What: "min-time option", Valid: "non-negative duration", Actual: parse.Quote(opts.MinTime)} } opts.minTime = d } return nil } func benchmark(fm *Frame, opts benchmarkOpts, f Callable) error { if err := opts.parse(); err != nil { return err } // Standard deviation is calculated using https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm var ( min = time.Duration(math.MaxInt64) max = time.Duration(math.MinInt64) runs int64 total time.Duration m2 float64 err error ) for { t0 := timeNow() err = f.Call(fm, NoArgs, NoOpts) if err != nil { break } dt := timeNow().Sub(t0) if min > dt { min = dt } if max < dt { max = dt } var oldDelta float64 if runs > 0 { oldDelta = float64(dt) - float64(total)/float64(runs) } runs++ total += dt if runs > 0 { newDelta := float64(dt) - float64(total)/float64(runs) m2 += oldDelta * newDelta } if opts.OnRunEnd != nil { newFm := fm.Fork() err = opts.OnRunEnd.Call(newFm, []any{dt.Seconds()}, NoOpts) if err != nil { break } } if runs >= int64(opts.MinRuns) && total >= opts.minTime { break } } if runs == 0 { return err } avg := total / time.Duration(runs) stddev := time.Duration(math.Sqrt(m2 / float64(runs))) if opts.OnEnd == nil { _, errOut := fmt.Fprintf(fm.ByteOutput(), "%v ± %v (min %v, max %v, %d runs)\n", avg, stddev, min, max, runs) if err == nil { err = errOut } } else { stats := vals.MakeMap( "avg", avg.Seconds(), "stddev", stddev.Seconds(), "min", min.Seconds(), "max", max.Seconds(), "runs", int64ToElv(runs)) newFm := fm.Fork() errOnEnd := opts.OnEnd.Call(newFm, []any{stats}, NoOpts) if err == nil { err = errOnEnd } } return err } func int64ToElv(i int64) any { if i <= int64(math.MaxInt) { return int(i) } else { return big.NewInt(i) } } elvish-0.21.0/pkg/eval/builtin_fn_time_test.elvts000066400000000000000000000115171465720375400221030ustar00rootroot00000000000000///////// # sleep # ///////// //mock-time-after ## number with no unit ## ~> sleep 0 slept for 0s ~> sleep 1 slept for 1s ~> sleep 0.1 slept for 100ms ~> sleep (num 0) slept for 0s ~> sleep (num 42) slept for 42s ~> sleep (num 1.7) slept for 1.7s ## number with unit ## ~> sleep 1.3s slept for 1.3s ~> sleep 0.1ms slept for 100µs ~> sleep 3h5m7s slept for 3h5m7s ## valid durations ## ~> sleep 1x Exception: invalid sleep duration [tty]:1:1-8: sleep 1x ~> sleep -7 Exception: sleep duration must be >= zero [tty]:1:1-8: sleep -7 ~> sleep -3h Exception: sleep duration must be >= zero [tty]:1:1-9: sleep -3h ~> sleep 1/2 slept for 500ms ~> sleep (num -7) Exception: sleep duration must be >= zero [tty]:1:1-14: sleep (num -7) ~> sleep [1] Exception: invalid sleep duration [tty]:1:1-9: sleep [1] ## can be interrupted ## //inject-time-after-with-sigint-or-skip ~> sleep 1s Exception: interrupted [tty]:1:1-8: sleep 1s //////// # time # //////// // Since runtime duration is non-deterministic, we only have some sanity checks // here. ~> time { echo foo } | var out time = (all) put $out ▶ foo ## &on-end ## ~> var duration = '' time &on-end={|x| set duration = $x } { echo foo } | var out = (all) put $out kind-of $duration ▶ foo ▶ number ## propagating exception ## ~> time { fail body } | nop (all) Exception: body [tty]:1:8-17: time { fail body } | nop (all) [tty]:1:1-19: time { fail body } | nop (all) ## propagating exception from &on-end ## ~> time &on-end={|_| fail on-end } { } Exception: on-end [tty]:1:19-30: time &on-end={|_| fail on-end } { } [tty]:1:1-35: time &on-end={|_| fail on-end } { } ## exception from body takes precedence ## ~> time &on-end={|_| fail on-end } { fail body } Exception: body [tty]:1:35-44: time &on-end={|_| fail on-end } { fail body } [tty]:1:1-45: time &on-end={|_| fail on-end } { fail body } ## bubbling output error ## ~> time { } >&- Exception: invalid argument [tty]:1:1-12: time { } >&- ///////////// # benchmark # ///////////// // These steps depend on the implementation detail that benchmark calls time.Now // once before a run and once after a run. ## default output ## //mock-benchmark-run-durations 1 2 ~> benchmark &min-runs=2 &min-time=2s { } 1.5s ± 500ms (min 1s, max 2s, 2 runs) ## &on-end ## //mock-benchmark-run-durations 1 2 ~> benchmark &min-runs=2 &min-time=2s &on-end=$put~ { } ▶ [&avg=(num 1.5) &max=(num 2.0) &min=(num 1.0) &runs=(num 2) &stddev=(num 0.5)] ## &min-runs determining number of runs ## //mock-benchmark-run-durations 1 2 1 2 ~> benchmark &min-runs=4 &min-time=0s &on-end={|m| put $m[runs]} { } ▶ (num 4) ## &min-time determining number of runs ## //mock-benchmark-run-durations 1 5 5 ~> benchmark &min-runs=0 &min-time=10s &on-end={|m| put $m[runs]} { } ▶ (num 3) ## &on-run-end ## //mock-benchmark-run-durations 1 2 1 ~> benchmark &min-runs=3 &on-run-end=$put~ &on-end={|m| } { } ▶ (num 1.0) ▶ (num 2.0) ▶ (num 1.0) ## body throws exception ## //mock-benchmark-run-durations 1 2 1 ~> var i = 0 benchmark { set i = (+ $i 1); if (== $i 3) { fail failure } } 1.5s ± 500ms (min 1s, max 2s, 2 runs) Exception: failure [tty]:2:46-58: benchmark { set i = (+ $i 1); if (== $i 3) { fail failure } } [tty]:2:1-61: benchmark { set i = (+ $i 1); if (== $i 3) { fail failure } } ## body throws exception on first run ## ~> benchmark { fail failure } Exception: failure [tty]:1:13-25: benchmark { fail failure } [tty]:1:1-26: benchmark { fail failure } ~> benchmark &on-end=$put~ { fail failure } Exception: failure [tty]:1:27-39: benchmark &on-end=$put~ { fail failure } [tty]:1:1-40: benchmark &on-end=$put~ { fail failure } ## &on-run-end throws exception ## //mock-benchmark-run-durations 1 ~> benchmark &on-run-end={|_| fail failure } { } 1s ± 0s (min 1s, max 1s, 1 runs) Exception: failure [tty]:1:28-40: benchmark &on-run-end={|_| fail failure } { } [tty]:1:1-45: benchmark &on-run-end={|_| fail failure } { } ## &on-end throws exception ## ~> benchmark &min-runs=2 &min-time=0s &on-end={|_| fail failure } { } Exception: failure [tty]:1:49-61: benchmark &min-runs=2 &min-time=0s &on-end={|_| fail failure } { } [tty]:1:1-66: benchmark &min-runs=2 &min-time=0s &on-end={|_| fail failure } { } ## option errors ## ~> benchmark &min-runs=-1 { } Exception: bad value: min-runs option must be non-negative integer, but is -1 [tty]:1:1-26: benchmark &min-runs=-1 { } ~> benchmark &min-time=abc { } Exception: bad value: min-time option must be duration string, but is abc [tty]:1:1-27: benchmark &min-time=abc { } ~> benchmark &min-time=-1s { } Exception: bad value: min-time option must be non-negative duration, but is -1s [tty]:1:1-27: benchmark &min-time=-1s { } ## bubbling output error ## //mock-benchmark-run-durations 1 ~> benchmark &min-runs=0 &min-time=0s { } >&- Exception: invalid argument [tty]:1:1-42: benchmark &min-runs=0 &min-time=0s { } >&- elvish-0.21.0/pkg/eval/builtin_ns.d.elv000066400000000000000000000040511465720375400177110ustar00rootroot00000000000000# A blackhole variable. # # Values assigned to it will be discarded. Referencing it always results in $nil. var _ #//skip-test # 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. # # See also [`src`](). var args # The boolean false value. var false # The special value used by `?()` to signal absence of exceptions. var ok # A special value useful for representing the lack of values. var nil # A list of search paths, kept in sync with `$E:PATH`. It is easier to use than # `$E:PATH`. var paths # The process ID of the current Elvish process. var pid # 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 # use path # for x [*/] { # tmp pwd = $x # if (path:is-dir .git) { # git pull # } # } # ``` # # Etymology: the `pwd` command. # # See also [`cd`](). var pwd # The boolean true value. var true # A map that exposes information about the Elvish binary. Running `put # $buildinfo | to-json` will produce the same output as `elvish -buildinfo # -json`. # # See also [`$version`](). var buildinfo # 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 # # ```elvish # 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`. # # See also [`$buildinfo`](). var version elvish-0.21.0/pkg/eval/builtin_ns.go000066400000000000000000000013271465720375400173110ustar00rootroot00000000000000package eval import ( "strconv" "syscall" "src.elv.sh/pkg/buildinfo" "src.elv.sh/pkg/eval/vars" ) 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"), "nop" + FnSuffix: vars.NewReadOnly(nopGoFn), }) func addBuiltinFns(fns map[string]any) { builtinNs.AddGoFns(fns) } elvish-0.21.0/pkg/eval/builtin_ns_test.elvts000066400000000000000000000033521465720375400211000ustar00rootroot00000000000000/////////////////////////////////////////////////////// # builtin module may be used implicitly or explicitly # /////////////////////////////////////////////////////// ~> put $true ▶ $true // regression test for b.elv.sh/1414 ~> use builtin put $builtin:true ▶ $true /////////////////////////////////// # builtin functions are read-only # /////////////////////////////////// ~> set return~ = { } Compilation error: variable $return~ is read-only [tty]:1:5-11: set return~ = { } ////////// # $paths # ////////// //each:unset-env PATH ## $E:PATH to $paths ## ~> use path ~> set E:PATH = /bin1$path:list-separator/bin2 ~> put $paths ▶ [/bin1 /bin2] ## $paths to $E:PATH ## ~> use path ~> set paths = [/bin1 /bin2] ~> eq $E:PATH /bin1$path:list-separator/bin2 ▶ $true ## invalid values ## ~> set paths = [$true] Exception: path must be string [tty]:1:5-9: set paths = [$true] ~> set paths = ["/invalid:;path"] Exception: path cannot contain NUL byte, colon on Unix or semicolon on Windows [tty]:1:5-9: set paths = ["/invalid:;path"] ~> set paths = ["/invalid\000path"] Exception: path cannot contain NUL byte, colon on Unix or semicolon on Windows [tty]:1:5-9: set paths = ["/invalid\000path"] //////// # $pwd # //////// //each:in-temp-dir ~> use os use path // Test both reading and writing $pwd. ~> var start = $pwd os:mkdir d set pwd = d eq $pwd (path:join $start d) ▶ $true ## bad assignment ## ~> set pwd = (num 1) Exception: path must be string [tty]:1:5-7: set pwd = (num 1) ## concrete value (Unix) ## //only-on unix ~> cd / put $pwd ▶ / ## concrete value (Windows) ## //only-on windows ~> cd C:\ put $pwd ▶ C:\ ## getwd error ## //mock-getwd-error can't get working directory ~> put $pwd ▶ /unknown/pwd elvish-0.21.0/pkg/eval/builtin_special.go000066400000000000000000000601571465720375400203170ustar00rootroot00000000000000package 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 ( "fmt" "os" "path/filepath" "reflect" "strings" "unicode/utf8" "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 } // PluginLoadError wraps a plugin loading error. type PluginLoadError struct { spec string err error } // Error implements the error interface. func (err PluginLoadError) Error() string { return "load as plugin: " + err.spec + ": " + err.err.Error() } // Unwrap returns the wrapped error. func (err PluginLoadError) Unwrap() error { return err.err } func init() { // Needed to avoid initialization loop builtinSpecials = map[string]compileBuiltin{ "var": compileVar, "set": compileSet, "tmp": compileTmp, "with": compileWith, "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' { LHS } [ '=' { Compound } ] func compileVar(cp *compiler, fn *parse.Form) effectOp { lhs, rhs := compileLHSOptionalRHS(cp, fn.Args, fn.To, newLValue) if rhs == nil { // Just create new variables, nothing extra to do at runtime. return nopOp{} } return &assignOp{fn.Range(), lhs, rhs, false} } // SetForm = 'set' { LHS } '=' { Compound } func compileSet(cp *compiler, fn *parse.Form) effectOp { lhs, rhs := compileLHSRHS(cp, fn.Args, fn.To, setLValue) return &assignOp{fn.Range(), lhs, rhs, false} } // TmpForm = 'tmp' { LHS } '=' { Compound } func compileTmp(cp *compiler, fn *parse.Form) effectOp { if len(cp.scopes) <= 1 { cp.errorpf(fn, "tmp may only be used inside a function") } lhs, rhs := compileLHSRHS(cp, fn.Args, fn.To, setLValue) return &assignOp{fn.Range(), lhs, rhs, true} } func compileWith(cp *compiler, fn *parse.Form) effectOp { if len(fn.Args) < 2 { cp.errorpfPartial(fn, "with requires at least two arguments") return nopOp{} } lastArg := fn.Args[len(fn.Args)-1] bodyNode, ok := cmpd.Lambda(lastArg) if !ok { // It's possible that we just haven't seen the last argument yet. cp.errorpfPartial(diag.PointRanging(fn.To), "last argument must be a lambda") return nopOp{} } assignNodes := fn.Args[:len(fn.Args)-1] firstNode, ok := cmpd.Primary(assignNodes[0]) if !ok { cp.errorpf(assignNodes[0], "argument must not be compound expressions") return nopOp{} } var assigns []withAssign if firstNode.Type == parse.List { assigns = make([]withAssign, len(assignNodes)) for i, assignNode := range assignNodes { p, ok := cmpd.Primary(assignNode) if !ok { cp.errorpf(assignNode, "argument must not be compound expressions") continue } if p.Type != parse.List { cp.errorpf(assignNode, "argument must be a list") continue } lhs, rhs := compileLHSRHS(cp, p.Elements, p.To, setLValue) assigns[i] = withAssign{assignNode.Range(), lhs, rhs} } } else { lhs, rhs := compileLHSRHS(cp, assignNodes, assignNodes[len(assignNodes)-1].To, setLValue) assigns = []withAssign{{diag.MixedRanging(assignNodes[0], assignNodes[len(assignNodes)-1]), lhs, rhs}} } return &withOp{fn.Range(), assigns, cp.primaryOp(bodyNode)} } type withOp struct { diag.Ranging assigns []withAssign bodyOp valuesOp } type withAssign struct { diag.Ranging lhs lvaluesGroup rhs valuesOp } func (op *withOp) exec(fm *Frame) (opExc Exception) { var restoreFuncs []func(*Frame) Exception defer func() { for i := len(restoreFuncs) - 1; i >= 0; i-- { exc := restoreFuncs[i](fm) if exc != nil && opExc == nil { opExc = exc } } }() for _, assign := range op.assigns { exc := doAssign(fm, assign, assign.lhs, assign.rhs, func(f func(*Frame) Exception) { restoreFuncs = append(restoreFuncs, f) }) if exc != nil { return exc } } body := execLambdaOp(fm, op.bodyOp) return fm.errorp(op, body.Call(fm.Fork(), NoArgs, NoOpts)) } // Finds LHS and RHS, compiling the RHS. Syntax: // // { LHS } '=' { Compound } func compileLHSRHS(cp *compiler, args []*parse.Compound, end int, lf lvalueFlag) (lvaluesGroup, valuesOp) { lhs, rhs := compileLHSOptionalRHS(cp, args, end, lf) if rhs == nil { cp.errorpfPartial(diag.PointRanging(end), "need = and right-hand-side") } return lhs, rhs } // Finds LHS and optional RHS, compiling RHS if it exists. Syntax: // // { LHS } [ '=' { Compound } ] func compileLHSOptionalRHS(cp *compiler, args []*parse.Compound, end int, lf lvalueFlag) (lvaluesGroup, valuesOp) { for i, cn := range args { if parse.SourceText(cn) == "=" { var rhs valuesOp if i == len(args)-1 { rhs = nopValuesOp{diag.PointRanging(end)} } else { rhs = seqValuesOp{ diag.MixedRanging(args[i+1], args[len(args)-1]), cp.compoundOps(args[i+1:])} } return cp.compileCompoundLValues(args[:i], lf), rhs } } return cp.compileCompoundLValues(args, lf), nil } 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 omit the dollar sign") continue } else if !parse.ValidLHSVariable(head, false) { cp.errorpf(cn, delArgMsg) continue } qname := head.Value var f effectOp ref := resolveVarRef(cp, qname, nil) if ref == nil { cp.errorpfPartial(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 the local scope 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 []any 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 := getArgs(cp, fn) name := args.get(0, "name").stringLiteral() bodyNode := args.get(1, "function body").lambda() if !args.finish() { return nil } // 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{fn.Args[0].Range(), index, op} } type fnOp struct { nameRange 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.nameRange, 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 { args := getArgs(cp, fn) spec := args.get(0, "module spec").stringLiteral() name := "" if args.has(1) { name = args.get(1, "module name").stringLiteral() } else { name = spec[strings.LastIndexByte(spec, '/')+1:] } if !args.finish() { return nil } 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.src.IsFile { dir = filepath.Dir(fm.src.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, PluginLoadError{spec, err} } sym, err := plug.Lookup("Ns") if err != nil { return nil, PluginLoadError{spec, err} } ns, ok := sym.(**Ns) if !ok { // plug.Lookup always returns a pointer t := reflect.TypeOf(sym).Elem() return nil, PluginLoadError{spec, fmt.Errorf("Ns symbol has wrong type %s", t)} } fm.Evaler.modules[path] = *ns return *ns, 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 } // 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 any = 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 := getArgs(cp, fn) var condNodes []*parse.Compound var bodyNodes []*parse.Primary i := 0 bodyName := "if body" for { condNodes = append(condNodes, args.get(i, "condition").any()) bodyNodes = append(bodyNodes, args.get(i+1, bodyName).thunk()) i += 2 if !args.hasKeyword(i, "elif") { break } i++ bodyName = "elif body" } elseBody := args.optionalKeywordBody(i, "else") if !args.finish() { return nil } condOps := cp.compoundOps(condNodes) bodyOps := cp.primaryOps(bodyNodes) var elseOp valuesOp if elseBody != nil { elseOp = cp.primaryOp(elseBody) } 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 exc != nil { return exc } if allTrue(condValues) { return fm.errorp(op, bodies[i].Call(fm.Fork(), NoArgs, NoOpts)) } } if op.elseOp != nil { return fm.errorp(op, elseFn.Call(fm.Fork(), NoArgs, NoOpts)) } return nil } func compileWhile(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) condNode := args.get(0, "condition").any() bodyNode := args.get(1, "while body").thunk() elseNode := args.optionalKeywordBody(2, "else") if !args.finish() { return nil } 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()) if exc != nil { return exc } if !allTrue(condValues) { break } iterated = true err := body.Call(fm.Fork(), 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(), NoArgs, NoOpts)) } return nil } func compileFor(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) varNode := args.get(0, "variable").any() iterNode := args.get(1, "iterable").any() bodyNode := args.get(2, "for body").thunk() elseNode := args.optionalKeywordBody(3, "else") if !args.finish() { return nil } 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 any) bool { iterated = true err := variable.Set(v) if err != nil { errElement = err return false } err = body.Call(fm.Fork(), 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(), NoArgs, NoOpts)) } return nil } func compileTry(cp *compiler, fn *parse.Form) effectOp { args := getArgs(cp, fn) bodyNode := args.get(0, "try body").thunk() i := 1 var catchVarNode *parse.Compound var catchNode *parse.Primary if args.hasKeyword(i, "catch") { i++ // Parse an optional lvalue into exceptVarNode. n := args.get(i, "variable or body").any() if _, ok := cmpd.StringLiteral(n); ok { catchVarNode = n i++ } catchNode = args.get(i, "catch body").thunk() i++ } elseNode := args.optionalKeywordBody(i, "else") if elseNode != nil { i += 2 } finallyNode := args.optionalKeywordBody(i, "finally") if !args.finish() { return nil } if elseNode != nil && catchNode == nil { cp.errorpf(fn, "try with an else block requires a catch block") } else if catchNode == nil && finallyNode == nil { cp.errorpfPartial(fn, "try must be followed by a catch block or a finally block") } var catchVar lvalue var bodyOp, catchOp, elseOp, finallyOp valuesOp bodyOp = cp.primaryOp(bodyNode) if catchVarNode != nil { catchVar = cp.compileOneLValue(catchVarNode, setLValue|newLValue) } if catchNode != nil { catchOp = cp.primaryOp(catchNode) } if elseNode != nil { elseOp = cp.primaryOp(elseNode) } if finallyNode != nil { finallyOp = cp.primaryOp(finallyNode) } return &tryOp{fn.Range(), bodyOp, catchVar, catchOp, elseOp, finallyOp} } type tryOp struct { diag.Ranging bodyOp valuesOp catchVar lvalue catchOp valuesOp elseOp valuesOp finallyOp valuesOp } func (op *tryOp) exec(fm *Frame) Exception { body := execLambdaOp(fm, op.bodyOp) var exceptVar vars.Var if op.catchVar.ref != nil { var err error exceptVar, err = derefLValue(fm, op.catchVar) if err != nil { return fm.errorp(op, err) } } catch := execLambdaOp(fm, op.catchOp) elseFn := execLambdaOp(fm, op.elseOp) finally := execLambdaOp(fm, op.finallyOp) err := body.Call(fm.Fork(), NoArgs, NoOpts) if err != nil { if catch != nil { if exceptVar != nil { err := exceptVar.Set(err.(Exception)) if err != nil { return fm.errorp(op.catchVar, err) } } err = catch.Call(fm.Fork(), NoArgs, NoOpts) } } else { if elseFn != nil { err = elseFn.Call(fm.Fork(), NoArgs, NoOpts) } } if finally != nil { errFinally := finally.Call(fm.Fork(), 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 := getArgs(cp, fn) name := args.get(0, "pragma name").stringLiteral() eq := args.get(1, "literal =").stringLiteral() if args.has(1) && eq != "=" { args.errorpf(fn.Args[1], "must be literal =") } valueNode := args.get(2, "pragma value").any() if !args.finish() { return nil } 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.errorpfPartial(valueNode, "invalid value for unknown-command: %s", parse.Quote(value)) } default: cp.errorpfPartial(fn.Args[0], "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.compileIndexingLValue(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.21.0/pkg/eval/builtin_special_test.elvts000066400000000000000000000540771465720375400221120ustar00rootroot00000000000000////////// # pragma # ////////// ~> pragma unknown-command Compilation error: need literal = [tty]:1:23: pragma unknown-command ~> pragma unknown-command = Compilation error: need pragma value [tty]:1:25: pragma unknown-command = ~> pragma unknown-command x Compilation error: must be literal = [tty]:1:24-24: pragma unknown-command x ~> pragma bad-name = some-value Compilation error: unknown pragma bad-name [tty]:1:8-15: pragma bad-name = some-value ~> pragma unknown-command = bad Compilation error: invalid value for unknown-command: bad [tty]:1:26-28: pragma unknown-command = bad // Actual effect of the unknown-command pragma is tested along with external // command resolution in compile_effect_test.elvts. /////// # var # /////// // Interaction between assignment and variable scoping is tested as part of // closure behavior in compile_value_test.elvts. ## declaring without assigning ## ~> var x put $x ▶ $nil ## Quoted variable name ## ~> var 'a/b' put $'a/b' ▶ $nil ## declaring one variable whose name ends in ":" ## ~> var a: ## declaring a variable whose name ends in "~" initializes it to the builtin nop ## ~> var cmd~ cmd &ignored-opt ignored-arg is $cmd~ $nop~ ▶ $true ## declaring multiple variables ## ~> var x y put $x $y ▶ $nil ▶ $nil ## declaring one variable with initial value ## ~> var x = foo put $x ▶ foo ## declaring multiple variables with initial values ## ~> var x y = foo bar put $x $y ▶ foo ▶ bar ## rest variable ## ~> var x @y z = a b c d put $x $y $z ▶ a ▶ [b c] ▶ d ## rest variable with empty RHS ## ~> var @x = put $x ▶ [] ## shadowing ## ~> var x = old fn f { put $x } var x = new put $x f ▶ new ▶ old ## RHS sees old variable when shadowing (https://b.elv.sh/1829) ## ~> var x = foo ~> var x = [$x] ~> put $x ▶ [foo] ## concurrent creation and access ## // Ensure that there is no race with "go test -race" ~> var x = 1 put $x | var y = (all) ## assignment errors when the RHS errors ## ~> var x = [][1] Exception: out of range: index must be from 0 to -1, but is 1 [tty]:1:9-13: var x = [][1] ## arity mismatch ## ~> var x = 1 2 Exception: arity mismatch: assignment right-hand-side must be 1 value, but is 2 values [tty]:1:1-11: var x = 1 2 ~> var x y = 1 Exception: arity mismatch: assignment right-hand-side must be 2 values, but is 1 value [tty]:1:1-11: var x y = 1 ~> var x y @z = 1 Exception: arity mismatch: assignment right-hand-side must be 2 or more values, but is 1 value [tty]:1:1-14: var x y @z = 1 ## variable name must not be empty ## ~> var '' Compilation error: variable name must not be empty [tty]:1:5-6: var '' ## variable name that must be quoted after $ must be quoted ## ~> var a/b Compilation error: lvalue must be valid literal variable names [tty]:1:5-7: var a/b ## multiple @ not allowed ## ~> var x @y @z = a b c d Compilation error: at most one rest variable is allowed [tty]:1:10-11: var x @y @z = a b c d ## non-local not allowed ## ~> var ns:a Compilation error: cannot create variable $ns:a; new variables can only be created in the current scope [tty]:1:5-8: var ns:a ## index not allowed ## ~> var a[0] Compilation error: new variable $a must not have indices [tty]:1:5-8: var a[0] ## composite expression not allowed ## ~> var a'b' Compilation error: lvalue may not be composite expressions [tty]:1:5-8: var a'b' /////// # set # /////// ## setting one variable ## ~> var x set x = foo put $x ▶ foo ## empty RHS is allowed ## ~> var x set @x = put $x ▶ [] ## variable must already exist ## ~> set x = foo Compilation error: cannot find variable $x [tty]:1:5-5: set x = foo ## list element assignment ## ~> var li = [foo bar]; set li[0] = 233; put $@li ▶ 233 ▶ bar ## variable in list assignment must already be defined ## // Regression test for b.elv.sh/889 ~> set y[0] = a Compilation error: cannot find variable $y [tty]:1:5-8: set y[0] = a ## map element assignment ## ~> var di = [&k=v] set di[k] = lorem set di[k2] = ipsum put $di[k] $di[k2] ▶ lorem ▶ ipsum ## nested map element assignment ## ~> var d = [&a=[&b=v]] put $d[a][b] set d[a][b] = u put $d[a][b] ▶ v ▶ u ## setting a non-exist environment variable ## //unset-env X ~> has-env X set E:X = x get-env X ▶ $false ▶ x ## map element assignment errors ## ~> var li = [foo]; set li[(fail foo)] = bar Exception: foo [tty]:1:25-32: var li = [foo]; set li[(fail foo)] = bar ~> var li = [foo]; set li[0 1] = foo bar Exception: multi indexing not implemented [tty]:1:21-27: var li = [foo]; set li[0 1] = foo bar ~> var li = [[]]; set li[1][2] = bar Exception: out of range: index must be from 0 to 0, but is 1 [tty]:1:20-27: var li = [[]]; set li[1][2] = bar ## assignment to read-only var is a compile-time error ## ~> set nil = 1 Compilation error: variable $nil is read-only [tty]:1:5-7: set nil = 1 ~> var a b set a true b = 1 2 3 Compilation error: variable $true is read-only [tty]:2:7-10: set a true b = 1 2 3 ~> set @true = 1 Compilation error: variable $true is read-only [tty]:1:5-9: set @true = 1 ~> var r set true @r = 1 Compilation error: variable $true is read-only [tty]:2:5-8: set true @r = 1 ~> var r set @r true = 1 Compilation error: variable $true is read-only [tty]:2:8-11: set @r true = 1 // Error conditions already covered by tests for var above are not repeated. ## = is required ## ~> var x; set x Compilation error: need = and right-hand-side [tty]:1:13: var x; set x ////////////////////// # error from Var.Set # ////////////////////// //add-bad-var bad 0 ~> set bad = foo Exception: bad var [tty]:1:5-7: set bad = foo ~> var a; set bad @a = foo Exception: bad var [tty]:1:12-14: var a; set bad @a = foo ~> var a; set a @bad = foo Exception: bad var [tty]:1:14-17: var a; set a @bad = foo ~> var a; set @a bad = foo Exception: bad var [tty]:1:15-17: var a; set @a bad = foo /////// # tmp # /////// ~> var x = foo put $x { tmp x = bar; put $x } put $x ▶ foo ▶ bar ▶ foo ## use outside function ## ~> var x; tmp x = y Compilation error: tmp may only be used inside a function [tty]:1:8-16: var x; tmp x = y ## non-existent variable ## ~> { tmp x = y } Compilation error: cannot find variable $x [tty]:1:7-7: { tmp x = y } ## used on unset environment variable ## //unset-env X ~> has-env X { tmp E:X = y; put $E:X } has-env X put $E:X ▶ $false ▶ y ▶ $false ▶ '' ## used on set environment variable ## //unset-env X ~> set-env X x { tmp E:X = y; put $E:X } get-env X put $E:X ▶ y ▶ x ▶ x ## use on existing map key (https://b.elv.sh/1515) ## ~> var m = [&k=old] ~> { tmp m[k] = new; put $m } ▶ [&k=new] ~> put $m ▶ [&k=old] ## use on non-existing map key (https://b.elv.sh/1515) ## ~> var m = [&] ~> { tmp m[k] = new; put $m } ▶ [&k=new] ~> put $m ▶ [&] ## use on list element (https://b.elv.sh/1515) ## ~> var a = [old] ~> { tmp a[0] = new; put $a } ▶ [new] ~> put $a ▶ [old] ## error setting ## //add-bad-var bad 0 ~> { tmp bad = foo } Exception: bad var [tty]:1:7-9: { tmp bad = foo } [tty]:1:1-17: { tmp bad = foo } # error restoring # //add-bad-var bad 1 ~> { tmp bad = foo; put after } ▶ after Exception: restore variable: bad var [tty]:1:7-9: { tmp bad = foo; put after } [tty]:1:1-28: { tmp bad = foo; put after } //////// # with # //////// ~> var x = old with x = new { put $x } put $x ▶ new ▶ old ## multiple assignments enclosed in lists ## ~> var x y = old-x old-y with [x = new-x] [y = new-y] { put $x $y } put $x $y ▶ new-x ▶ new-y ▶ old-x ▶ old-y ## variables are restored if body throws exception ## ~> var x = old with [x = new] { fail foo } Exception: foo [tty]:2:18-26: with [x = new] { fail foo } ~> put $x ▶ old ## exception setting variable restores previously set variables ## //add-bad-var bad 0 ~> var x = old with [x = new] [bad = new] { } Exception: bad var [tty]:2:17-19: with [x = new] [bad = new] { } ~> put $x ▶ old ## exception restoring variable is propagated and doesn't affect restoring other variables ## //add-bad-var bad 1 ~> var x = old with [x = new] [bad = new] { } Exception: restore variable: bad var [tty]:2:17-19: with [x = new] [bad = new] { } ~> put $x ▶ old ## two few arguments ## ~> with Compilation error: with requires at least two arguments [tty]:1:1-4: with ~> with { } Compilation error: with requires at least two arguments [tty]:1:1-8: with { } ## last argument not lambda ## ~> var x with x = val foobar Compilation error: last argument must be a lambda [tty]:2:20: with x = val foobar ## compound expressions ## ~> with a'x' = foo { } Compilation error: argument must not be compound expressions [tty]:1:6-9: with a'x' = foo { } ~> var x with [x = a] a'y' { } Compilation error: argument must not be compound expressions [tty]:2:14-17: with [x = a] a'y' { } ## list followed by non-list ## ~> var x with [x = a] y { } Compilation error: argument must be a list [tty]:2:14-14: with [x = a] y { } /////// # del # /////// ~> var x = 1 del x ## variable can't be used after deleted ## ~> var x = 1 del x echo $x Compilation error: variable $x not found [tty]:3:6-7: echo $x ## deleting environment variable ## //set-env TEST_ENV test_value ~> has-env TEST_ENV del E:TEST_ENV has-env TEST_ENV ▶ $true ▶ $false ## deleting variable whose name contains special characters ## ~> var 'a/b' = foo del 'a/b' ## deleting element ## ~> var x = [&k=v &k2=v2] del x[k2] keys $x ▶ k ~> var x = [[&k=v &k2=v2]]; del x[0][k2] keys $x[0] ▶ k ## deleting nonexistent variable ## ~> del x Compilation error: no variable $x [tty]:1:5-5: del x ## deleting element of nonexistent variable ## ~> del x[0] Compilation error: no variable $x [tty]:1:5-8: del x[0] ## deleting non-local variable ## ~> var a: = (ns [&b=$nil]) del a:b Compilation error: only variables in the local scope or E: can be deleted [tty]:2:5-7: del a:b ## variable name given with $ ## ~> var x = 1 del $x Compilation error: arguments to del must omit the dollar sign [tty]:2:5-6: del $x ## variable name not given as a single primary expression ## ~> var ab = 1 del a'b' Compilation error: arguments to del must be variable or variable elements [tty]:2:5-8: del a'b' ## variable name not a string ## ~> del [a] Compilation error: arguments to del must be variable or variable elements [tty]:1:5-7: del [a] ## variable name has sigil ## ~> var x = []; del @x Compilation error: arguments to del must be variable or variable elements [tty]:1:17-18: var x = []; del @x ## variable name not quoted when it should be ## ~> var 'a/b' = foo del a/b Compilation error: arguments to del must be variable or variable elements [tty]:2:5-7: del a/b ## index is multiple values ## ~> var x = [&k1=v1 &k2=v2] del x[k1 k2] Exception: index must evaluate to a single value in argument to del [tty]:2:7-11: del x[k1 k2] ## index expression throws ## ~> var x = [&k] del x[(fail x)] Exception: x [tty]:2:8-13: del x[(fail x)] ## value does not support element removal ## ~> var x = (num 1) del x[k] Exception: value does not support element removal [tty]:2:5-7: del x[k] // TODO: Fix the stack trace so that it points to "x[k]" instead of "x[k" ## intermediate element does not exist ## ~> var x = [&] del x[k][0] Exception: no such key: k [tty]:2:5-5: del x[k][0] /////// # and # /////// ~> and $true $false ▶ $false ~> and a b ▶ b ~> and $false b ▶ $false ~> and $true b ▶ b ## short circuit ## ~> var x = a and $false (x = b) put $x ▶ $false ▶ a ## exception propagation ## ~> and a (fail x) Exception: x [tty]:1:8-13: and a (fail x) ## output error is bubbled ## ~> and a >&- Exception: port does not support value output [tty]:1:1-9: and a >&- ////// # or # ////// ~> or $true $false ▶ $true ~> or a b ▶ a ~> or $false b ▶ b ~> or $true b ▶ $true ## short circuit ## ~> var x = a; or $true (x = b); put $x ▶ $true ▶ a ## exception ## ~> or $false (fail x) Exception: x [tty]:1:12-17: or $false (fail x) ## output error is bubbled ## ~> or a >&- Exception: port does not support value output [tty]:1:1-8: or a >&- //////////// # coalesce # //////////// ~> coalesce a b ▶ a ~> coalesce $nil b ▶ b ~> coalesce $nil $nil ▶ $nil ~> coalesce ▶ $nil ## short circuit ## ~> coalesce a (fail foo) ▶ a ## exception propagation ## ~> coalesce $nil (fail foo) Exception: foo [tty]:1:16-23: coalesce $nil (fail foo) ## output error is bubbled ## ~> coalesce a >&- Exception: port does not support value output [tty]:1:1-14: coalesce a >&- //////////////////////////////// # special forms require thunks # //////////////////////////////// // Regression test for b.elv.sh/1456. // // This only tests "for"; the other special forms use the same utility under the // hood and are not repeated. ~> for x [] {|arg| } Compilation error: for body must not have arguments [tty]:1:10-17: for x [] {|arg| } ~> for x [] {|&opt=val| } Compilation error: for body must not have options [tty]:1:10-22: for x [] {|&opt=val| } ////// # if # ////// ~> if true { put then } ▶ then ~> if $false { put then } else { put else } ▶ else ~> if $false { put 1 } elif $false { put 2 } else { put 3 } ▶ 3 ~> if $false { put 2 } elif true { put 2 } else { put 3 } ▶ 2 ## exception in condition expression ## ~> if (fail x) { } Exception: x [tty]:1:5-10: if (fail x) { } /////// # try # /////// ~> try { nop } catch { put bad } else { put good } ▶ good ~> try { fail tr } catch - { put bad } else { put good } ▶ bad ~> try { fail tr } finally { put final } ▶ final Exception: tr [tty]:1:7-14: try { fail tr } finally { put final } ~> try { fail tr } catch { fail ex } finally { put final } ▶ final Exception: ex [tty]:1:25-32: try { fail tr } catch { fail ex } finally { put final } ~> try { fail tr } catch { put ex } finally { fail final } ▶ ex Exception: final [tty]:1:44-54: try { fail tr } catch { put ex } finally { fail final } ~> try { fail tr } catch { fail ex } finally { fail final } Exception: final [tty]:1:45-55: try { fail tr } catch { fail ex } finally { fail final } ## must have catch to use else ## ~> try { fail tr } else { echo else } Compilation error: try with an else block requires a catch block [tty]:1:1-34: try { fail tr } else { echo else } ## must have catch or finally ## ~> try { fail tr } Compilation error: try must be followed by a catch block or a finally block [tty]:1:1-15: try { fail tr } ## rest variable not allowed ## ~> try { nop } catch @a { } Compilation error: rest variable not allowed [tty]:1:19-20: try { nop } catch @a { } ## readonly var as a target for the "catch" clause ## ~> try { fail reason } catch nil { } Compilation error: variable $nil is read-only [tty]:1:27-29: try { fail reason } catch nil { } ## quoted var name ## ~> try { fail hard } catch 'x=' { put $'x='[reason][type] } ▶ fail ## regression test: "try { } catch" is a syntax error, but it should not panic ## ~> try { } catch Compilation error: need variable or body [tty]:1:14: try { } catch ///////// # while # ///////// ~> var x = (num 0) while (< $x 4) { put $x; set x = (+ $x 1) } ▶ (num 0) ▶ (num 1) ▶ (num 2) ▶ (num 3) ## break ## ~> var x = (num 0) while (< $x 4) { put $x; break } ▶ (num 0) ## continue ## ~> var x = (num 0) while (< $x 4) { put $x; set x = (+ $x 1); continue; put bad } ▶ (num 0) ▶ (num 1) ▶ (num 2) ▶ (num 3) ## exception in body ## ~> var x = 0; while (< $x 4) { fail haha } Exception: haha [tty]:1:29-38: var x = 0; while (< $x 4) { fail haha } ## exception in condition ## ~> while (fail x) { } Exception: x [tty]:1:8-13: while (fail x) { } ## else branch - not taken ## ~> var x = 0; while (< $x 4) { put $x; set x = (+ $x 1) } else { put bad } ▶ 0 ▶ (num 1) ▶ (num 2) ▶ (num 3) ## else branch - taken ## ~> while $false { put bad } else { put good } ▶ good /////// # for # /////// ~> for x [tempora mores] { put 'O '$x } ▶ 'O tempora' ▶ 'O mores' ## break ## ~> for x [a] { break } else { put $x } ## else ## ~> for x [a] { put $x } else { put $x } ▶ a ## continue ## ~> for x [a b] { put $x; continue; put $x; } ▶ a ▶ b ## else ## ~> for x [] { } else { put else } ▶ else ~> for x [a] { } else { put else } ## propagating exception ## ~> for x [a] { fail foo } Exception: foo [tty]:1:13-21: for x [a] { fail foo } ## can't create new variable non-local variable ## ~> for no-such-namespace:x [a b] { } Compilation error: cannot create variable $no-such-namespace:x; new variables can only be created in the current scope [tty]:1:5-23: for no-such-namespace:x [a b] { } ## can't use non-existent variable ## ~> var a: = (ns [&]) for a:b [] { } Exception: no variable $a:b [tty]:2:5-7: for a:b [] { } ## exception when evaluating iterable ## ~> for x [][0] { } Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:7-11: for x [][0] { } ## more than one iterable ## ~> for x (put a b) { } Exception: arity mismatch: value being iterated must be 1 value, but is 2 values [tty]:1:7-15: for x (put a b) { } ## non-iterable value ## ~> for x (num 0) { } Exception: cannot iterate number [tty]:1:1-17: for x (num 0) { } ////// # fn # ////// ~> fn f {|x| put x=$x'.' }; f lorem; f ipsum ▶ 'x=lorem.' ▶ 'x=ipsum.' ## recursive functions with fn ## // Regression test for b.elv.sh/1206. ~> fn f {|n| if (== $n 0) { num 1 } else { * $n (f (- $n 1)) } }; f 3 ▶ (num 6) ## swallowing exception thrown by return ## ~> fn f { put a; return; put b }; f ▶ a ## error when evaluating the lambda ## ~> fn f {|&opt=(fail x)| } Exception: x [tty]:1:14-19: fn f {|&opt=(fail x)| } /////// # use # /////// ## basic usage ## //tmp-lib-dir ~> echo 'var name = ipsum' > $lib/lorem.elv ~> use lorem put $lorem:name ▶ ipsum ## imports are lexically scoped ## //tmp-lib-dir ~> echo 'var name = ipsum' > $lib/lorem.elv ~> { use lorem } put $lorem:name Compilation error: variable $lorem:name not found [tty]:2:5-15: put $lorem:name ## prefers lib dir that appear earlier ## //two-tmp-lib-dirs ~> echo 'echo lib1/shadow' > $lib1/shadow.elv ~> echo 'echo lib2/shadow' > $lib2/shadow.elv ~> use shadow lib1/shadow ## use of imported variable is captured in upvalue ## //tmp-lib-dir ~> echo 'var name = ipsum' > $lib/lorem.elv ~> use lorem { put $lorem:name } ▶ ipsum ## use of imported function is also captured in upvalue ## //tmp-lib-dir ~> echo 'var name = ipsum; fn put-name { put $name }' > $lib/lorem.elv ~> { use lorem; { lorem:put-name } } ▶ ipsum ## use of module in subdirectory ## //tmp-lib-dir // TODO: Use os:mkdir-all when it's available. ~> use os os:mkdir $lib/a os:mkdir $lib/a/b echo 'var name = a/b/c' > $lib/a/b/c.elv ~> use a/b/c put $c:name ▶ a/b/c ## module is cached after first use ## //tmp-lib-dir ~> echo 'put has-init' > $lib/has-init.elv ~> use has-init ▶ has-init ~> use has-init // Init code is not run again ## renaming module ## //tmp-lib-dir // TODO: Use os:mkdir-all when it's available. ~> use os os:mkdir $lib/a os:mkdir $lib/a/b echo 'var name = a/b/c' > $lib/a/b/c.elv ~> use a/b/c mod put $mod:name ▶ a/b/c ## modules can be used multiple times with different aliases ## //tmp-lib-dir ~> echo 'var name = ipsum' > $lib/lorem.elv ~> use lorem use lorem lorem2 put $lorem:name $lorem2:name ▶ ipsum ▶ ipsum ## variable referencing a module can be shadowed ## //tmp-lib-dir // TODO: Use os:mkdir-all when it's available. ~> use os os:mkdir $lib/a os:mkdir $lib/a/b echo 'var name = c' > $lib/c.elv echo 'var name = a/b/c' > $lib/a/b/c.elv ~> use c put $c:name use a/b/c put $c:name ▶ c ▶ a/b/c ## relative uses ## //tmp-lib-dir ~> use os os:mkdir $lib/a os:mkdir $lib/a/b echo 'var name = ipsum' > $lib/lorem.elv echo 'var name = a/b/c' > $lib/a/b/c.elv echo 'use ./c; var c = $c:name; use ../../lorem; var lorem = $lorem:name' > $lib/a/b/x.elv ~> use a/b/x; put $x:c $x:lorem ▶ a/b/c ▶ ipsum ## relative uses from the REPL ## // Relative uses from the REPL is relative to the working directory. //in-temp-dir ~> echo 'var name = ipsum' > lorem.elv ~> use ./lorem put $lorem:name ▶ ipsum ## variables in the REPL scope is invisible from modules ## //tmp-lib-dir ~> echo 'put $x' > $lib/put-x.elv // We have to do this since the exception stack trace contains $lib, which is a // temporary directory that changes across runs. // // TODO: Print the whole error message (but without the filename) when // exceptions support that level of introspection. ~> try { use put-x } catch e { echo has exception } has exception ## invalid UTF-8 in module file ## //tmp-lib-dir ~> echo "\xff" > $lib/invalid-utf8.elv // We have to do this since the exception stack trace contains $lib, which is a // temporary directory that changes across runs. // // TODO: Print the whole error message (but without the filename) when // exceptions support that level of introspection. ~> try { use invalid-utf8 } catch e { echo has exception } has exception ## unknown module spec ## ~> use unknown Exception: no such module: unknown [tty]:1:1-11: use unknown ~> use ./unknown Exception: no such module: ./unknown [tty]:1:1-13: use ./unknown ~> use ../unknown Exception: no such module: ../unknown [tty]:1:1-14: use ../unknown ## wrong number of arguments ## ~> use Compilation error: need module spec [tty]:1:4: use ~> use a b c Compilation error: superfluous arguments [tty]:1:9-9: use a b c ## circular dependency ## //tmp-lib-dir ~> echo 'var pre = apre; use b; put $b:pre $b:post; var post = apost' > $lib/a.elv echo "var pre = bpre; use a; put $a:pre $a:post; var post = bpost" > $lib/b.elv ~> use a ▶ apre ▶ $nil ▶ bpre ▶ bpost ## importing module triggers check for deprecated features ## // Regression test for b.elv.sh/1072 //tmp-lib-dir //deprecation-level 21 ~> echo '..' > $lib/dep.elv // Only show the first line to avoid showing the file path, which contains $lib // and changes across runs. ~> use dep 2>&1 | take 1 | to-lines Deprecation: implicit cd is deprecated; use cd or location mode instead ## module may mutate REPL namespace ## // Regression test for b.elv.sh/1225 //tmp-lib-dir //add-var-in-builtin ~> echo 'var foo = bar; add-var foo $foo' > $lib/a.elv ~> use a ~> keys $a: ▶ foo ~> put $foo ▶ bar elvish-0.21.0/pkg/eval/callable.go000066400000000000000000000006251465720375400167020ustar00rootroot00000000000000package eval // Callable wraps the Call method. type Callable interface { // Call calls the receiver in a Frame with arguments and options. Call(fm *Frame, args []any, opts map[string]any) error } var ( // NoArgs is an empty argument list. It can be used as an argument to Call. NoArgs = []any{} // NoOpts is an empty option map. It can be used as an argument to Call. NoOpts = map[string]any{} ) elvish-0.21.0/pkg/eval/chdir_test.go000066400000000000000000000023761465720375400173000ustar00rootroot00000000000000package eval_test import ( "os" "testing" "src.elv.sh/pkg/env" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) func TestChdir(t *testing.T) { dst := testutil.TempDir(t) ev := NewEvaler() argDirInBefore, argDirInAfter := "", "" ev.BeforeChdir = append(ev.BeforeChdir, func(dir string) { argDirInBefore = dir }) ev.AfterChdir = append(ev.AfterChdir, 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 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() { must.Chdir(wd) } } elvish-0.21.0/pkg/eval/closure.go000066400000000000000000000124361465720375400166220ustar00rootroot00000000000000package 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" ) // Closure is a function defined with 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 []any Src parse.Source DefRange diag.Ranging op effectOp newLocal []staticVarInfo captured *Ns } var ( _ Callable = &Closure{} _ vals.PseudoMap = &Closure{} ) // Kind returns "fn". func (*Closure) Kind() string { return "fn" } // Equal compares by address. func (c *Closure) Equal(rhs any) 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)) } // Call calls a closure. func (c *Closure) Call(fm *Frame, args []any, opts map[string]any) 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.src = c.Src fm.defers = new([]func(*Frame) Exception) exc := c.op.exec(fm) excDefer := fm.runDefers() // TODO: Combine exc and excDefer if both are not nil if excDefer != nil && exc == nil { exc = excDefer } return exc } // 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 := nopGoFn return vars.FromPtr(&val) case strings.HasSuffix(name, NsSuffix): val := &Ns{} 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 vals.MakeListSlice(cf.c.ArgNames) } func (cf closureFields) RestArg() string { return strconv.Itoa(cf.c.RestArg) } func (cf closureFields) OptNames() vals.List { return vals.MakeListSlice(cf.c.OptNames) } func (cf closureFields) Src() parse.Source { return cf.c.Src } 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.Src.Code[r.From:r.To] } func (cf closureFields) Def() string { return cf.c.Src.Code[cf.c.DefRange.From:cf.c.DefRange.To] } elvish-0.21.0/pkg/eval/closure_test.elvts000066400000000000000000000023141465720375400204030ustar00rootroot00000000000000/////////// # closure # /////////// ## value operations ## ~> kind-of { } ▶ fn ~> eq { } { } ▶ $false ~> var x = { }; put [&$x= foo][$x] ▶ foo ## arity check ## ~> {|x| } a b Exception: arity mismatch: arguments must be 1 value, but is 2 values [tty]:1:1-10: {|x| } a b ~> {|x y| } a Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-10: {|x y| } a ~> {|x y @rest| } a Exception: arity mismatch: arguments must be 2 or more values, but is 1 value [tty]:1:1-16: {|x y @rest| } a ## unsupported option ## ~> {|&valid=1| } &bad=1 Exception: unsupported option: bad [tty]:1:1-20: {|&valid=1| } &bad=1 ~> {|&valid1=1 &valid2=2| } &bad1=1 &bad2=2 Exception: unsupported options: bad1, bad2 [tty]:1:1-40: {|&valid1=1 &valid2=2| } &bad1=1 &bad2=2 ## introspection ## ~> put {|a b| }[arg-names] ▶ [a b] ~> put {|@r| }[rest-arg] ▶ 0 ~> put {|&opt=def| }[opt-names] ▶ [opt] ~> put {|&opt=def| }[opt-defaults] ▶ [def] ~> put { body }[body] ▶ 'body ' ~> put {|x @y| body }[def] ▶ '{|x @y| body }' ~> put { body }[src][code] ▶ 'put { body }[src][code]' ## body of fn-defined function ## // Regression test for https://b.elv.sh/1126. ~> fn f { body }; put $f~[body] ▶ 'body ' elvish-0.21.0/pkg/eval/compile_effect.go000066400000000000000000000313061465720375400201070ustar00rootroot00000000000000package eval import ( "context" "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/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.Canceled() { 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.Canceled() { return fm.errorp(op, ErrInterrupted) } if op.bg { fm = fm.Fork() fm.ctx = context.Background() 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() 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 any, pipelineChanBufferSize) sendStop := make(chan struct{}) sendError := new(error) readerGone := new(atomic.Bool) 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) input.readerGone.Store(true) } 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 { redirOps := cp.redirOps(n.Redirs) body := cp.formBody(n) return &formOp{n.Range(), 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} } } 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 { cp.autofixUnresolvedVar(head + FnSuffix) if cp.currentPragma().unknownCommandIsExternal || fsutil.DontSearch(head) { headOp = literalValues(n.Head, NewExternalCmd(head)) } else { cp.errorpfPartial(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 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. // 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 []any 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]any) exc := cmd.optsOp.exec(fm, func(k, v any) 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.ReprPlain(value)}) } func allTrue(vs []any) bool { for _, v := range vs { if !vals.Bool(v) { return false } } return true } 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: &ErrPortDoesNotSupportValueOutput} case src >= len(fm.ports) || fm.ports[src] == nil: return fm.errorp(op, InvalidFD{FD: 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.ReprPlain(src), err) } fm.ports[dst] = fileRedirPort(op.mode, f, true) case vals.File: fm.ports[dst] = fileRedirPort(op.mode, src, false) case vals.Map, vals.StructMap: var srcFile *os.File switch op.mode { case parse.Read: v, err := vals.Index(src, "r") f, ok := v.(*os.File) if err != nil || !ok { return fm.errorp(op.srcOp, errs.BadValue{ What: "map for input redirection", Valid: "map with file in the 'r' field", Actual: vals.ReprPlain(src)}) } srcFile = f case parse.Write: v, err := vals.Index(src, "w") f, ok := v.(*os.File) if err != nil || !ok { return fm.errorp(op.srcOp, errs.BadValue{ What: "map for output redirection", Valid: "map with file in the 'w' field", Actual: vals.ReprPlain(src)}) } srcFile = f default: return fm.errorpf(op, "can only use < or > with maps") } fm.ports[dst] = fileRedirPort(op.mode, srcFile, false) default: return fm.errorp(op.srcOp, errs.BadValue{ What: "redirection source", Valid: "string, file or map", 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: &ErrPortDoesNotSupportValueOutput, } } // 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.ReprPlain(value)}) } 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.21.0/pkg/eval/compile_effect_test.elvts000066400000000000000000000240211465720375400216720ustar00rootroot00000000000000///////// # chunk # ///////// ## empty chunk ## ~> ## outputs of pipelines in a chunk are concatenated ## ~> put x; put y; put z ▶ x ▶ y ▶ z ## a failed pipeline cause the whole chunk to fail ## ~> put a; fail bad; put b ▶ a Exception: bad [tty]:1:8-15: put a; fail bad; put b //////////// # pipeline # //////////// ## pure byte pipeline on Unix ## //only-on unix ~> echo "Albert\nAllan\nAlbraham\nBerlin" | sed s/l/1/g | grep e A1bert Ber1in ## pure byte pipeline on Windows ## //only-on windows ~> echo "Albert\nAllan\nAlbraham\nBerlin" | findstr e Albert Berlin ## pure value pipeline ## ~> put 233 42 19 | each {|x|+ $x 10} ▶ (num 243) ▶ (num 52) ▶ (num 29) ## pipeline draining ## ~> range 100 | put x ▶ x // TODO: Add a useful hybrid pipeline sample ## reader gone ## // Internal commands writing to byte output raises ReaderGone when the reader has // exited, which is then suppressed by the pipeline. ~> while $true { echo y } | nop ~> var reached = $false { while $true { echo y }; reached = $true } | nop put $reached ▶ $false // Similar for value output. ~> while $true { put y } | nop ~> var reached = $false { while $true { put y }; reached = $true } | nop put $reached ▶ $false ## reader gone from SIGPIPE ## //only-on unix // External commands terminated by SIGPIPE due to reader exiting early raise // ReaderGone, which is then suppressed by the pipeline. ~> yes | true ~> var reached = $false { yes; reached = $true } | true put $reached ▶ $false /////////////////////// # background pipeline # /////////////////////// //each:eval use file ## basic behavior ## ~> set notify-bg-job-success = $false var p = (file:pipe) { print foo > $p; file:close $p[w] }& slurp < $p; file:close $p[r] ▶ foo ## notification ## //recv-bg-job-notification-in-global ~> set notify-bg-job-success = $true var p = (file:pipe) fn f { file:close $p[w] } f & slurp < $p; file:close $p[r] recv-bg-job-notification ▶ '' ▶ 'job f & finished' ## notification with exception ## //recv-bg-job-notification-in-global ~> set notify-bg-job-success = $true var p = (file:pipe) fn f { file:close $p[w]; fail foo } f & slurp < $p; file:close $p[r] recv-bg-job-notification ▶ '' ▶ 'job f & finished, errors = foo' /////////// # command # /////////// ~> put foo ▶ foo ## error conditions ## // head is not a single value ~> {put put} foo Exception: arity mismatch: command must be 1 value, but is 2 values [tty]:1:1-9: {put put} foo // head is not callable or string containing slash ~> [] foo Exception: bad value: command must be callable or string containing slash, but is [] [tty]:1:1-2: [] foo // argument throws ~> put [][1] Exception: out of range: index must be from 0 to -1, but is 1 [tty]:1:5-9: put [][1] // option key is not string ~> put &[]=[] Exception: bad value: option key must be string, but is list [tty]:1:1-10: put &[]=[] // option evaluation throws ~> put &x=[][1] Exception: out of range: index must be from 0 to -1, but is 1 [tty]:1:8-12: put &x=[][1] ## regression test for b.elv.sh/1204 ## // Ensure that the arguments of special forms are not accidentally compiled // twice. ~> nop (and (use builtin)) nop $builtin:echo~ ///////////////////////////// # external command as value # ///////////////////////////// ~> kind-of (external true) ▶ fn ~> repr (external true) ~> put [&(external true)=$true][(external true)] ▶ $true //////////////////////////////// # external commands invocation # //////////////////////////////// ## common behavior ## // Doesn't support options ~> (external foo) &option Exception: external commands don't accept elvish options [tty]:1:1-22: (external foo) &option ## external command from PATH ## //in-temp-dir //unset-env PATH ~> use os os:mkdir bin to-lines ['#!/bin/sh' 'echo hello'] > bin/say-hello os:chmod 0o700 bin/say-hello to-lines ['@echo hello'] > bin/say-hello.bat set paths = [$pwd/bin] ~> say-hello hello // Explicit e: ~> e:say-hello hello // Dynamic string does not work for finding external commands from PATH ~> var x = say-hello $x Exception: bad value: command must be callable or string containing slash, but is say-hello [tty]:2:1-2: $x // Command searching is affected by the unknown-command pragma ~> { pragma unknown-command = disallow; say-hello } Compilation error: unknown command disallowed by current pragma [tty]:1:38-46: { pragma unknown-command = disallow; say-hello } ~> { pragma unknown-command = disallow; { say-hello } } Compilation error: unknown command disallowed by current pragma [tty]:1:40-48: { pragma unknown-command = disallow; { say-hello } } ~> { pragma unknown-command = external; say-hello } hello // But explicit e: is always allowed ~> { pragma unknown-command = disallow; e:say-hello } hello ## external command relative to working directory ## //in-temp-dir ~> use os fn make-echo-script {|name msg| to-lines ['#!/bin/sh' 'echo '$msg] > $name os:chmod 0o700 $name to-lines ['@echo '$msg] > $name.bat } make-echo-script say-hello hello os:mkdir lorem make-echo-script lorem/ipsum 'lorem ipsum' ~> ./say-hello hello ~> ./lorem/ipsum lorem ipsum ~> lorem/ipsum lorem ipsum // Explicit e: ~> e:./say-hello hello ~> e:./lorem/ipsum lorem ipsum ~> e:lorem/ipsum lorem ipsum // Dynamic string ~> var x = ./say-hello $x hello ~> var x = ./lorem/ipsum $x lorem ipsum ~> var x = lorem/ipsum $x lorem ipsum // Relative external commands are not affected by the unknown-command pragma ~> { pragma unknown-command = disallow; ./say-hello } hello ~> { pragma unknown-command = disallow; lorem/ipsum } lorem ipsum ~> { pragma unknown-command = disallow; var x = ./say-hello; $x } hello ## non-existent command on Unix ## //only-on unix //unset-env PATH ~> set paths = [] ~> nonexistent-command Exception: exec: "nonexistent-command": executable file not found in $PATH [tty]:1:1-19: nonexistent-command ## non-existent command on Windows ## //only-on windows //unset-env PATH ~> set paths = [] ~> nonexistent-command Exception: exec: "nonexistent-command": executable file not found in %PATH% [tty]:1:1-19: nonexistent-command /////////////// # implicit cd # /////////////// //in-temp-dir //deprecation-level 21 ~> use os use path os:mkdir new-dir var old-pwd = $pwd ~> ./new-dir Deprecation: implicit cd is deprecated; use cd or location mode instead [tty]:1:1-9: ./new-dir ~> eq $pwd (path:join $old-pwd new-dir) ▶ $true /////////////// # redirection # /////////////// //each:in-temp-dir ## output and input redirection ## ~> echo 233 > out1 slurp < out1 ▶ "233\n" ## append ## ~> echo 1 > out; echo 2 >> out; slurp < out ▶ "1\n2\n" ## read and write ## // TODO: Add a meaningful use case that actually uses both read and write. ~> echo 233 <> out1 slurp < out1 ▶ "233\n" ## redirections from special form ## ~> for x [lorem ipsum] { echo $x } > out2 slurp < out2 ▶ "lorem\nipsum\n" ## using numeric FDs as source and destination ## ~> { echo foobar >&2 } 2> out3 slurp < out3 ▶ "foobar\n" ## using named FDs as source and destination ## ~> echo 233 stdout> out1 slurp stdin< out1 ▶ "233\n" ## using named FDs (stderr) as source and destination ## ~> { echo foobar >&stderr } stderr> out4 slurp < out4 ▶ "foobar\n" ## using a new FD as source throws an exception ## ~> echo foo >&4 Exception: invalid fd: 4 [tty]:1:10-12: echo foo >&4 ## using a new FD as destination is OK, and makes it available ## ~> { echo foo >&4 } 4>out5 slurp < out5 ▶ "foo\n" ## using a new FD for external command is OK ## // Regression test against b.elv.sh/788. //only-on unix // TODO: Enable for Windows too. ~> /bin/sh -c 'echo ok' 5 use file echo haha > out3 var f = (file:open out3) slurp <$f file:close $f ▶ "haha\n" ## redirections from pipe objects ## ~> use file var p = (file:pipe) echo haha > $p file:close $p[w] slurp < $p file:close $p[r] ▶ "haha\n" ## regression test for b.elv.sh/1010 ## // Don't hang when iterating over input from a file. ~> echo abc > bytes each $echo~ < bytes abc ~> echo def > bytes only-values < bytes | count ▶ (num 0) ## writing value output to file throws an exception ## ~> put foo >a Exception: port does not support value output [tty]:1:1-10: put foo >a ## writing value output to closed port throws an exception ## ~> put foo >&- Exception: port does not support value output [tty]:1:1-11: put foo >&- ## invalid redirection destination ## ~> echo []> test Exception: bad value: redirection destination must be fd name or number, but is [] [tty]:1:6-7: echo []> test ## invalid fd redirection source ## ~> echo >&test Exception: bad value: redirection source must be fd name or number or '-', but is test [tty]:1:8-11: echo >&test ## invalid redirection source ## ~> echo > [] Exception: bad value: redirection source must be string, file or map, but is list [tty]:1:8-9: echo > [] ## invalid map for redirection ## ~> echo < [&] Exception: bad value: map for input redirection must be map with file in the 'r' field, but is [&] [tty]:1:8-10: echo < [&] ~> echo > [&] Exception: bad value: map for output redirection must be map with file in the 'w' field, but is [&] [tty]:1:8-10: echo > [&] ## exception when evaluating source or destination ## ~> echo > (fail foo) Exception: foo [tty]:1:9-16: echo > (fail foo) ~> echo (fail foo)> file Exception: foo [tty]:1:7-14: echo (fail foo)> file //////////////// # stack traces # //////////////// // Stack traces of increasing depths. ~> fail oops Exception: oops [tty]:1:1-9: fail oops ~> fn f { fail oops } f Exception: oops [tty]:1:8-17: fn f { fail oops } [tty]:2:1-1: f ~> fn f { fail oops } fn g { f } g Exception: oops [tty]:1:8-17: fn f { fail oops } [tty]:2:8-9: fn g { f } [tty]:3:1-1: g // Error thrown before execution. ~> fn f { } f a Exception: arity mismatch: arguments must be 0 values, but is 1 value [tty]:2:1-3: f a // Error from builtin. ~> count 1 2 3 Exception: arity mismatch: arguments must be 0 to 1 values, but is 3 values [tty]:1:1-11: count 1 2 3 elvish-0.21.0/pkg/eval/compile_lvalue.go000066400000000000000000000156601465720375400201500ustar00rootroot00000000000000package eval import ( "errors" "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" ) // Parsed group of lvalues. type lvaluesGroup struct { lvalues []lvalue // Index of the rest variable within lvalues. If there is no rest variable, // the index is -1. rest int } // Parsed lvalue. type lvalue struct { diag.Ranging ref *varRef indexOps []valuesOp ends []int } type lvalueFlag uint const ( setLValue lvalueFlag = 1 << iota newLValue ) func (cp *compiler) compileCompoundLValues(ns []*parse.Compound, f lvalueFlag) lvaluesGroup { g := lvaluesGroup{nil, -1} for _, n := range ns { if len(n.Indexings) != 1 { cp.errorpf(n, "lvalue may not be composite expressions") break } more := cp.compileIndexingLValue(n.Indexings[0], f) if more.rest == -1 { g.lvalues = append(g.lvalues, more.lvalues...) } else if g.rest != -1 { cp.errorpf(n, "at most one rest variable is allowed") } else { g.rest = len(g.lvalues) + more.rest g.lvalues = append(g.lvalues, more.lvalues...) } } return g } var dummyLValuesGroup = lvaluesGroup{[]lvalue{{}}, -1} func (cp *compiler) compileIndexingLValue(n *parse.Indexing, f lvalueFlag) lvaluesGroup { if !parse.ValidLHSVariable(n.Head, true) { cp.errorpf(n.Head, "lvalue must be valid literal variable names") return dummyLValuesGroup } varUse := n.Head.Value sigil, qname := SplitSigil(varUse) if qname == "" { cp.errorpfPartial(n, "variable name must not be empty") return dummyLValuesGroup } 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", parse.Quote(qname)) return dummyLValuesGroup } } if ref == nil { if f&newLValue == 0 { cp.autofixUnresolvedVar(qname) cp.errorpfPartial(n, "cannot find variable $%s", parse.Quote(qname)) return dummyLValuesGroup } if len(n.Indices) > 0 { cp.errorpf(n, "new variable $%s must not have indices", parse.Quote(qname)) return dummyLValuesGroup } 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 { cp.errorpf(n, "cannot create variable $%s; "+ "new variables can only be created in the current scope", parse.Quote(qname)) return dummyLValuesGroup } } 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 temp bool } func (op *assignOp) exec(fm *Frame) Exception { var rc restoreCollector if op.temp { rc = fm.addDefer } return doAssign(fm, op, op.lhs, op.rhs, rc) } func doAssign(fm *Frame, r diag.Ranger, lhs lvaluesGroup, rhs valuesOp, rc restoreCollector) Exception { // Evaluate LHS. variables := make([]vars.Var, len(lhs.lvalues)) for i, lvalue := range lhs.lvalues { variable, err := derefLValue(fm, lvalue) if err != nil { return fm.errorp(lhs.lvalues[i], err) } variables[i] = variable } // Evaluate RHS. values, exc := rhs.exec(fm) if exc != nil { return exc } // Now perform assignment. if rest := lhs.rest; rest == -1 { if len(variables) != len(values) { return fm.errorp(r, errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: len(variables), ValidHigh: len(variables), Actual: len(values)}) } for i, variable := range variables { exc := set(fm, lhs.lvalues[i], variable, values[i], rc) if exc != nil { return exc } } } else { if len(values) < len(variables)-1 { return fm.errorp(r, errs.ArityMismatch{What: "assignment right-hand-side", ValidLow: len(variables) - 1, ValidHigh: -1, Actual: len(values)}) } for i := 0; i < rest; i++ { exc := set(fm, lhs.lvalues[i], variables[i], values[i], rc) if exc != nil { return exc } } restOff := len(values) - len(variables) exc := set(fm, lhs.lvalues[rest], variables[rest], vals.MakeList(values[rest:rest+restOff+1]...), rc) if exc != nil { return exc } for i := rest + 1; i < len(variables); i++ { exc := set(fm, lhs.lvalues[i], variables[i], values[i+restOff], rc) if exc != nil { return exc } } } return nil } type restoreCollector func(func(*Frame) Exception) // Sets the variable to the value. // // If rc is non-empty, calls it with a function that restores the original value // after setting the variable. func set(fm *Frame, r diag.Ranger, variable vars.Var, value any, rc restoreCollector) Exception { var restore func(*Frame) Exception if rc != nil { restore = save(r, variable) } err := variable.Set(value) if err != nil { return fm.errorp(r, err) } if rc != nil { rc(restore) } return nil } // Returns a function that restores a variable to its current value. func save(r diag.Ranger, variable vars.Var) func(*Frame) Exception { if head := vars.HeadOfElement(variable); head != nil { // Needed for temporary assignments to elements (https://b.elv.sh/1515). variable = head } // Handle "unsettable" variables (currently just environment variables) // correctly. if unsettable, ok := variable.(vars.UnsettableVar); ok && !unsettable.IsSet() { return func(fm *Frame) Exception { if err := unsettable.Unset(); err != nil { return fm.errorpf(r, "unset variable: %w", err) } return nil } } saved := variable.Get() return func(fm *Frame) Exception { err := variable.Set(saved) if err != nil { return fm.errorpf(r, "restore variable: %w", 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.src.Code[lv.From:lv.To]) } if len(lv.indexOps) == 0 { return variable, nil } indices := make([]any, 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.21.0/pkg/eval/compile_value.go000066400000000000000000000331001465720375400177610ustar00rootroot00000000000000package 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) ([]any, Exception) } var outputCaptureBufferSize = 16 // Can be mutated for testing. var getHome = fsutil.GetHome 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) ([]any, Exception) { home, err := getHome("") if err != nil { return nil, fm.errorp(op, err) } return []any{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) ([]any, 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([]any, 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([]any, 0, len(vs)) for _, v := range vs { if gp, ok := v.(globPattern); ok { results, err := doGlob(fm.Context(), gp) 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 []any, us []any, f func(any, any) (any, error)) ([]any, error) { ws := make([]any, 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 any) (any, 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 := 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/xxx. Replace the first segment with the home // directory of the specified user. dir, err := getHome(seg.Data) if err != nil { return nil, err } v.Segments[0] = glob.Literal{Data: dir} return v, nil } case glob.Slash: dir, err := 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) ([]any, 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([]any, 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.autofixUnresolvedVar(qname) // The variable name might be a prefix of a valid variable name. // Ideally, we'd want to match the variable name to all possible // names to check if that's actually the case, but it's a bit // expensive and let's call this good enough for now. cp.errorpfPartial(n, "variable $%s not found", parse.Quote(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 := []any{ 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) ([]any, Exception) { variable := deref(fm, op.ref) if variable == nil { return nil, fm.errorpf(op, "variable $%s not found", parse.Quote(op.qname)) } value := variable.Get() if op.explode { vs, err := vals.Collect(value) return vs, fm.errorp(op, err) } return []any{value}, nil } type listOp struct { diag.Ranging subops []valuesOp } func (op listOp) exec(fm *Frame) ([]any, 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.Conj(moreValue) } } return []any{list}, nil } type exceptionCaptureOp struct { diag.Ranging subop effectOp } func (op exceptionCaptureOp) exec(fm *Frame) ([]any, Exception) { exc := op.subop.exec(fm) if exc == nil { return []any{OK}, nil } return []any{exc}, nil } type outputCaptureOp struct { diag.Ranging subop effectOp } func (op outputCaptureOp) exec(fm *Frame) ([]any, Exception) { outPort, collect, err := ValueCapturePort() if err != nil { return nil, fm.errorp(op, err) } exc := op.subop.exec(fm.forkWithOutput(outPort)) return collect(), exc } func (cp *compiler) lambda(n *parse.Primary) valuesOp { // 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)) seenName := make(map[string]bool) 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.errorpfPartial(arg, "argument name must not be empty") } if sigil == "@" { if restArg != -1 { cp.errorpf(arg, "only one argument may have @ prefix") } restArg = i } if name != "_" { if seenName[name] { cp.errorpfPartial(arg, "duplicate argument name '%s'", name) } else { seenName[name] = true } } 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.errorpfPartial(opt.Key, "option name must not be empty") } optNames[i] = name if opt.Value == nil { cp.errorpfPartial(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.src} } 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) ([]any, 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([]any, 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 []any{&Closure{op.argNames, op.restArg, op.optNames, optDefaults, op.srcMeta, op.Range(), op.subop, op.newLocal, capture}}, nil } type mapOp struct { diag.Ranging pairsOp *mapPairsOp } func (op mapOp) exec(fm *Frame) ([]any, Exception) { m := vals.EmptyMap exc := op.pairsOp.exec(fm, func(k, v any) Exception { m = m.Assoc(k, v) return nil }) if exc != nil { return nil, exc } return []any{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 any) 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 []any } func (op literalValuesOp) exec(*Frame) ([]any, Exception) { return op.values, nil } func literalValues(r diag.Ranger, vs ...any) valuesOp { return literalValuesOp{r.Range(), vs} } type seqValuesOp struct { diag.Ranging subops []valuesOp } func (op seqValuesOp) exec(fm *Frame) ([]any, Exception) { var values []any 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) ([]any, Exception) { return nil, nil } func evalForValue(fm *Frame, op valuesOp, what string) (any, 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.21.0/pkg/eval/compile_value_test.elvts000066400000000000000000000176651465720375400215720ustar00rootroot00000000000000//////////// # compound # //////////// ~> put {fi,elvi}sh{1.0,1.1} ▶ fish1.0 ▶ fish1.1 ▶ elvish1.0 ▶ elvish1.1 ## empty compound evaluates to '' ## ~> put {} ▶ '' ~> put [&k=][k] ▶ '' ## exception from any component is propagated ## ~> put a{[][1]} Exception: out of range: index must be from 0 to -1, but is 1 [tty]:1:7-11: put a{[][1]} ## error in concatenating the values throws an exception ## ~> put []a Exception: cannot concatenate list and string [tty]:1:5-7: put []a ## error when applying tilde throws an exception ## ~> put ~[] Exception: tilde doesn't work on value of type list [tty]:1:5-7: put ~[] //////////// # indexing # //////////// ~> put [a b c][2] ▶ c ~> put [][0] Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:5-9: put [][0] ~> put [&key=value][key] ▶ value ~> put [&key=value][bad] Exception: no such key: bad [tty]:1:5-21: put [&key=value][bad] ~> put (fail x)[a] Exception: x [tty]:1:6-11: put (fail x)[a] ~> put [foo][(fail x)] Exception: x [tty]:1:12-17: put [foo][(fail x)] //////////////// # list literal # //////////////// ~> put [a b c] ▶ [a b c] ~> put [] ▶ [] ## exception from element expression is propagated ## ~> put [ [][0] ] Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:7-11: put [ [][0] ] /////////////// # map literal # /////////////// ~> put [&key=value] ▶ [&key=value] ~> put [&] ▶ [&] // Keys and values may evaluate to multiple values as long as their numbers // match. ~> put [&{a b}={foo bar}] ▶ [&a=foo &b=bar] ## exception from key or value is propagated ## ~> put [ &[][0]=a ] Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:8-12: put [ &[][0]=a ] ~> put [ &a=[][0] ] Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:10-14: put [ &a=[][0] ] ## error if number of keys and values in a single pair does not match ## ~> put [&{a b}={foo bar lorem}] Exception: 2 keys but 3 values [tty]:1:6-27: put [&{a b}={foo bar lorem}] ////////////////// # string literal # ////////////////// ~> put 'such \"''literal' ▶ 'such \"''literal' ~> put "much \n\033[31;1m$cool\033[m" ▶ "much \n\e[31;1m$cool\e[m" ///////// # tilde # ///////// //each:with-temp-home ~> eq ~ $E:HOME ▶ $true ~> eq ~/src $E:HOME/src ▶ $true // Trailing slash is retained ~> eq ~/src/ $E:HOME/src/ ▶ $true ## tilde and wildcard ## ~> echo > ~/file1 echo > ~/file2 ~> eq [~/*] [~/file1 ~/file2] ▶ $true ## ~other doesn't add superfluous trailing slash ## // Regression test for b.elv.sh/1246 //mock-one-other-home ~> eq ~other $other-home ▶ $true ## ~other and glob ## // Regression test for b.elv.sh/793 //mock-one-other-home ~> echo > $other-home/file1 echo > $other-home/file2 ~> eq [~other/*] [$other-home/file1 $other-home/file2] ▶ $true ## unknown user ## //mock-no-other-home ~> put ~bad/* Exception: don't know home of bad [tty]:1:5-10: put ~bad/* ## ~* is an error ## // TODO: This should be a compilation error ~> put ~* Exception: cannot determine user name from glob pattern [tty]:1:5-6: put ~* ## error in GetHome ## //mock-get-home-error fake error ~> put ~ Exception: fake error [tty]:1:5-5: put ~ ~> put ~/foo Exception: fake error [tty]:1:5-9: put ~/foo ~> put ~/* Exception: fake error [tty]:1:5-7: put ~/* //////////// # wildcard # //////////// ~> put *** Compilation error: bad wildcard: "***" [tty]:1:5-7: put *** // More tests in glob_test.elvts ////////////////// # output capture # ////////////////// ~> put (put lorem ipsum) ▶ lorem ▶ ipsum ~> put (print "lorem\nipsum") ▶ lorem ▶ ipsum // \r\n is also supported as a line separator ~> print "lorem\r\nipsum\r\n" | all ▶ lorem ▶ ipsum ///////////////////// # exception capture # ///////////////////// ## Exception capture ## ~> bool ?(nop) ▶ $true ~> bool ?(e:false) ▶ $false //////////////// # variable use # //////////////// ## basic usage ## ~> var x = foo put $x ▶ foo ## must exist before use ## ~> put $x Compilation error: variable $x not found [tty]:1:5-6: put $x ~> put $x[0] Compilation error: variable $x not found [tty]:1:5-6: put $x[0] ## variable use in compound expr ## ~> var x = world put 'Hello, '$x'!' ▶ 'Hello, world!' ## exploding with $@ ## ~> var x = [elvish rules] put $@x ▶ elvish ▶ rules ## unqualified name resolves to local name before upvalue ## ~> var x = outer; { var x = inner; put $x } ▶ inner ## unqualified name resolves to upvalue if no local name exists ## ~> var x = outer; { put $x } ▶ outer ## unqualified name resolves to builtin if no local name or upvalue exists ## ~> put $true ▶ $true ## names like $:foo are reserved for now. ## ~> var x = val; put $:x Compilation error: variable $:x not found [tty]:1:18-20: var x = val; put $:x ## pseudo-namespace E: for environment variables ## //unset-env x ~> set-env x value put $E:x ▶ value ~> set E:x = new-value get-env x ▶ new-value ## colons after E: are part of the variable name ## //unset-env a:b ~> set-env a:b value put $E:a:b ▶ value ~> set E:a:b = new-value get-env a:b ▶ new-value ## pseudo-namespace e: for external commands ## // Resolution always succeeds regardless of whether the command exists ~> put $e:true~ ▶ // Colons are always considered part of the name ~> put $e:a:b~ ▶ ## namespace access ## ~> var ns: = (ns [&a= val]) put $ns:a ▶ val ## multi-level namespace access ## ~> var ns: = (ns [&a:= (ns [&b= val])]) put $ns:a:b ▶ val ## module name access is checked at runtime ## ~> use os ~> put $os:non-existent-variable Exception: variable $os:non-existent-variable not found [tty]:1:5-29: put $os:non-existent-variable /////////// # closure # /////////// ~> {|| } ~> {|x| put $x} foo ▶ foo ## assigning to captured variable ## ~> var x = lorem; {|| put $x; set x = ipsum }; put $x ▶ lorem ▶ ipsum ## Assigning to element of captured variable ## ~> var x = [a]; { set x[0] = b }; put $x[0] ▶ b ## shadowing ## ~> var x = ipsum; { var x = lorem; put $x }; put $x ▶ lorem ▶ ipsum ## shadowing by argument ## ~> var x = ipsum; {|x| put $x; set x = BAD } lorem; put $x ▶ lorem ▶ ipsum ## closure semantics ## ~> fn f { var x = (num 0) put { set x = (+ $x 1) } { put $x } } ~> var inc1 put1 = (f); $put1; $inc1; $put1 ▶ (num 0) ▶ (num 1) ~> var inc2 put2 = (f); $put2; $inc2; $put2 ▶ (num 0) ▶ (num 1) ## rest argument. ## ~> {|x @xs| put $x $xs } a b c ▶ a ▶ [b c] ~> {|a @b c| put $a $b $c } a b c d ▶ a ▶ [b c] ▶ d ## options ## ~> {|a &k=v| put $a $k } foo &k=bar ▶ foo ▶ bar ## option default value ## ~> {|a &k=v| put $a $k } foo ▶ foo ▶ v ## option must have default value ## ~> {|&k| } Compilation error: option must have default value [tty]:1:4-4: {|&k| } ## exception when evaluating option default value ## ~> {|&a=[][0]| } Exception: out of range: index must be from 0 to -1, but is 0 [tty]:1:6-10: {|&a=[][0]| } ## option default value must be one value ## ~> {|&a=(put foo bar)| } Exception: arity mismatch: option default value must be 1 value, but is 2 values [tty]:1:6-18: {|&a=(put foo bar)| } ## argument name must be unqualified ## ~> {|a:b| } Compilation error: argument name must be unqualified [tty]:1:3-5: {|a:b| } ## argument name must not be empty ## ~> {|''| } Compilation error: argument name must not be empty [tty]:1:3-4: {|''| } ~> {|@| } Compilation error: argument name must not be empty [tty]:1:3-3: {|@| } ## argument name must not be duplicated ## ~> {|a a| } Compilation error: duplicate argument name 'a' [tty]:1:5-5: {|a a| } // but multiple _s are OK ~> nop {|_ a _| } ## option name must be unqualified ## ~> {|&a:b=1| } Compilation error: option name must be unqualified [tty]:1:4-6: {|&a:b=1| } ## option name must not be empty ## ~> {|&''=b| } Compilation error: option name must not be empty [tty]:1:4-5: {|&''=b| } ## should not have multiple rest arguments ## ~> {|@a @b| } Compilation error: only one argument may have @ prefix [tty]:1:6-7: {|@a @b| } elvish-0.21.0/pkg/eval/compiler.go000066400000000000000000000130121465720375400167470ustar00rootroot00000000000000package eval import ( "fmt" "io" "strings" "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 // Names of internal modules. modules []string // Destination of warning messages. This is currently only used for // deprecation messages. warn io.Writer // Deprecation registry. deprecations deprecationRegistry // Information about the source. src parse.Source // Compilation errors. errors []*CompilationError // Suggested code to fix potential issues found during compilation. autofixes []string } type scopePragma struct { unknownCommandIsExternal bool } func compile(b, g *staticNs, modules []string, tree parse.Tree, w io.Writer) (nsOp, []string, error) { g = g.clone() cp := &compiler{ b, []*staticNs{g}, []*staticUpNs{new(staticUpNs)}, []*scopePragma{{unknownCommandIsExternal: true}}, modules, w, newDeprecationRegistry(), tree.Source, nil, nil} chunkOp := cp.chunkOp(tree.Root) return nsOp{chunkOp, g}, cp.autofixes, diag.PackErrors(cp.errors) } 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) } } type CompilationError = diag.Error[CompilationErrorTag] // CompilationErrorTag parameterizes [diag.Error] to define [CompilationError]. type CompilationErrorTag struct{} func (CompilationErrorTag) ErrorTag() string { return "compilation error" } // Reports a compilation error. func (cp *compiler) errorpf(r diag.Ranger, format string, args ...any) { cp.errorpfInner(r, fmt.Sprintf(format, args...), false) } // Reports a compilation error, and mark it as partial iff the end of r happens // to coincide with the end of the source code. func (cp *compiler) errorpfPartial(r diag.Ranger, format string, args ...any) { cp.errorpfInner(r, fmt.Sprintf(format, args...), r.Range().To == len(cp.src.Code)) } func (cp *compiler) errorpfInner(r diag.Ranger, msg string, partial bool) { cp.errors = append(cp.errors, &CompilationError{ Message: msg, Context: *diag.NewContext(cp.src.Name, cp.src.Code, r), // TODO: This criteria is too strict and only captures a small subset of // partial compilation errors. Partial: partial, }) } // UnpackCompilationErrors returns the constituent compilation errors if the // given error contains one or more compilation errors. Otherwise it returns // nil. func UnpackCompilationErrors(e error) []*CompilationError { if errs := diag.UnpackErrors[CompilationErrorTag](e); len(errs) > 0 { return errs } 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 := 22 switch name { // We don't have any deprecated builtins targeted for 0.22 yet, but keep // this code here so that the code doesn't get stale. This function is only // called for symbols that actually resolve to builtins, so having a fake // one here is harmless. case "foo~": msg = `the "foo" command is deprecated; use "bar" instead` default: return } cp.deprecate(r, msg, minLevel) } type deprecationTag struct{} func (deprecationTag) ErrorTag() string { return "deprecation" } func (cp *compiler) deprecate(r diag.Ranger, msg string, minLevel int) { if cp.warn == nil || r == nil { return } dep := deprecation{cp.src.Name, r.Range(), msg} if prog.DeprecationLevel >= minLevel && cp.deprecations.register(dep) { err := diag.Error[deprecationTag]{ Message: msg, Context: *diag.NewContext(cp.src.Name, cp.src.Code, r.Range())} fmt.Fprintln(cp.warn, err.Show("")) } } // Given a variable that doesn't resolve, add any applicable autofixes. func (cp *compiler) autofixUnresolvedVar(qname string) { if len(cp.modules) == 0 { return } first, _ := SplitQName(qname) mod := strings.TrimSuffix(first, ":") if mod != first && sliceContains(cp.modules, mod) { cp.autofixes = append(cp.autofixes, "use "+mod) } } elvish-0.21.0/pkg/eval/compiler_test.elvts000066400000000000000000000013101465720375400205340ustar00rootroot00000000000000//////////////////////////// # compile-time deprecation # //////////////////////////// //deprecation-level 21 //in-temp-dir // This test will need to be frequently updated as deprecated commands get // removed. // // Deprecations of other builtins are implemented in the same way, so we // don't test them repeatedly ~> use os os:mkdir foo ~> ./foo Deprecation: implicit cd is deprecated; use cd or location mode instead [tty]:1:1-5: ./foo /////////////////////////////// # multiple compilation errors # /////////////////////////////// ~> echo $x; echo $y Multiple compilation errors: variable $x not found [tty]:1:6-7: echo $x; echo $y variable $y not found [tty]:1:15-16: echo $x; echo $y elvish-0.21.0/pkg/eval/compiler_test.go000066400000000000000000000046711465720375400200210ustar00rootroot00000000000000package eval_test import ( "testing" "unicode/utf8" "github.com/google/go-cmp/cmp" "src.elv.sh/pkg/diag" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) var autofixTests = []struct { Name string Code string WantAutofixes []string }{ { Name: "get variable from unimported builtin module", Code: "echo $mod1:foo", WantAutofixes: []string{"use mod1"}, }, { Name: "set variable from unimported builtin module", Code: "set mod1:foo = bar", WantAutofixes: []string{"use mod1"}, }, { Name: "tmp set variable from unimported builtin module", Code: "tmp mod1:foo = bar", WantAutofixes: []string{"use mod1"}, }, { Name: "call command from unimported builtin module", Code: "mod1:foo", WantAutofixes: []string{"use mod1"}, }, { Name: "no autofix for using variable from imported module", Code: "use mod1; echo $mod1:foo", WantAutofixes: nil, }, { Name: "no autofix for using variable from non-builtin module", Code: "echo $mod-external:foo", WantAutofixes: nil, }, } func TestAutofix(t *testing.T) { ev := NewEvaler() ev.AddModule("mod1", &Ns{}) for _, tc := range autofixTests { t.Run(tc.Name, func(t *testing.T) { _, autofixes, _ := ev.Check(parse.Source{Name: "[test]", Code: tc.Code}, nil) if diff := cmp.Diff(tc.WantAutofixes, autofixes); diff != "" { t.Errorf("autofixes (-want +got):\n%s", diff) } }) } } // TODO: Turn this into a fuzz test. func TestPartialCompilationError(t *testing.T) { for _, code := range transcriptCodes { testPartialError(t, code, func(src parse.Source) []*CompilationError { _, _, err := NewEvaler().Check(src, nil) return UnpackCompilationErrors(err) }) } } // TODO: Deduplicate this against a similar helper in pkg/parse/fuzz_test.go func testPartialError[T diag.ErrorTag](t *testing.T, full string, fn func(src parse.Source) []*diag.Error[T]) { if !utf8.ValidString(full) { t.Skip("not valid UTF-8") } errs := fn(parse.Source{Name: "fuzz.elv", Code: full}) if len(errs) > 0 { t.Skip("code itself has error") } // If code has no error, then every prefix of it (as long as it's valid // UTF-8) should have either no errors or only partial errors. for i := range full { if i == 0 { continue } prefix := full[:i] errs := fn(parse.Source{Name: "fuzz.elv", Code: prefix}) for _, err := range errs { if !err.Partial { t.Errorf("\n%s\n===========\nnon-partial error: %v\nfull code:\n===========\n%s\n", prefix, err, full) } } } } elvish-0.21.0/pkg/eval/deprecation.go000066400000000000000000000011161465720375400174340ustar00rootroot00000000000000package 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.21.0/pkg/eval/errs/000077500000000000000000000000001465720375400155645ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/errs/errs.go000066400000000000000000000046641465720375400171000ustar00rootroot00000000000000// 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 { 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.21.0/pkg/eval/errs/errs_test.go000066400000000000000000000022211465720375400201220ustar00rootroot00000000000000package 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", }, { 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.21.0/pkg/eval/eval.d.elv000066400000000000000000000035251465720375400164770ustar00rootroot00000000000000#//skip-test # 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 # ~> set before-chdir = [{|dir| echo "Going to change to "$dir", pwd is "$pwd }] # ~> set 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> # ``` # # **Note**: The use of `echo` above is for illustrative purposes. When Elvish # is used interactively, the working directory may be changed in location mode # or navigation mode, and outputs from `echo` can garble the terminal. If you # are writing a plugin that works with the interactive mode, it's better to use # [`edit:notify`](edit.html#edit:notify). # # See also [`$before-chdir`](). var after-chdir # A list of functions to run before changing directory. These functions are always # called with the new working directory. # # See also [`$after-chdir`](). var before-chdir # A list of functions to run before Elvish exits. var before-exit # Number of background jobs. var num-bg-jobs # Whether to notify success of background jobs, defaulting to `$true`. # # Failures of background jobs are always notified. var notify-bg-job-success #//skip-test #// The test framework hardcodes value out indicators. # A string put before value outputs (such as those of `put`). Defaults to # `'▶ '`. Example: # # ```elvish-transcript # ~> put lorem ipsum # ▶ lorem # ▶ ipsum # ~> set value-out-indicator = 'val> ' # ~> put lorem ipsum # val> lorem # val> ipsum # ``` # # Note that you almost always want some trailing whitespace for readability. var value-out-indicator elvish-0.21.0/pkg/eval/eval.go000066400000000000000000000265061465720375400161000ustar00rootroot00000000000000// Package eval handles evaluation of parsed Elvish code and provides runtime // facilities. package eval import ( "context" "fmt" "io" "os" "strconv" "sync" "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" ) 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 ) // 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. PreExitHooks []func() // Chdir hooks, exposed indirectly as $before-chdir and $after-chdir. BeforeChdir, AfterChdir []func(string) // 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) // Path to the rc file, and path to the rc file actually evaluated. These // are not used by the Evaler itself right now; they are here so that they // can be exposed to the runtime: module. RcPath, EffectiveRcPath 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 } // NewEvaler creates a new Evaler. func NewEvaler() *Evaler { builtin := builtinNs.Ns() newListVar := func(l vals.List) vars.PtrVar { return vars.FromPtr(&l) } beforeExitHookElvish := newListVar(vals.EmptyList) beforeChdirElvish := newListVar(vals.EmptyList) afterChdirElvish := newListVar(vals.EmptyList) 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.PreExitHooks = []func(){func() { CallHook(ev, nil, "before-exit", beforeExitHookElvish.Get().(vals.List)) }} ev.BeforeChdir = []func(string){func(path string) { CallHook(ev, nil, "before-chdir", beforeChdirElvish.Get().(vals.List), path) }} ev.AfterChdir = []func(string){func(path string) { CallHook(ev, nil, "after-chdir", afterChdirElvish.Get().(vals.List), path) }} ev.ExtendBuiltin(BuildNs(). AddVar("pwd", NewPwdVar(ev)). AddVar("before-exit", beforeExitHookElvish). AddVar("before-chdir", beforeChdirElvish). AddVar("after-chdir", 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() any { return strconv.Itoa(ev.getNumBgJobs()) })). AddVar("args", vars.FromGet(func() any { return ev.Args }))) // Install the "builtin" module after extension is complete. ev.modules["builtin"] = ev.builtin return ev } // PreExit runs all pre-exit hooks. func (ev *Evaler) PreExit() { for _, hook := range ev.PreExitHooks { hook() } } // 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()) } // DeleteFromGlobal deletes names from the global namespace. func (ev *Evaler) DeleteFromGlobal(names map[string]struct{}) { ev.mu.Lock() defer ev.mu.Unlock() g := ev.global.clone() for i := range g.infos { if _, ok := names[g.infos[i].name]; ok { g.infos[i].deleted = true } } ev.global = g } // 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()) } // ReplaceBuiltin replaces the builtin namespace. It should only be used in // tests. func (ev *Evaler) ReplaceBuiltin(ns *Ns) { ev.mu.Lock() defer ev.mu.Unlock() ev.builtin = 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 } // 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 { // Context that can be used to cancel the evaluation. Interrupts context.Context // 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 // 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(), nil, 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 function. Args []any // Options to pass to the function. Opts map[string]any // 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()) { intCtx := cfg.Interrupts if intCtx == nil { intCtx = context.Background() } ports := fillDefaultDummyPorts(cfg.Ports) fm := &Frame{ev, src, cfg.Global, new(Ns), nil, intCtx, ports, nil, false} return fm, func() { 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, autofixes, and // compilation error. It always tries to compile the code even if there is a // parse error. If w is not nil, deprecation messages are written to it. func (ev *Evaler) Check(src parse.Source, w io.Writer) (error, []string, error) { tree, parseErr := parse.Parse(src, parse.Config{WarningWriter: w}) autofixes, compileErr := ev.CheckTree(tree, w) return parseErr, autofixes, compileErr } // CheckTree checks the given parsed source tree for autofixes and compilation // errors. If w is not nil, deprecation messages are written to it. func (ev *Evaler) CheckTree(tree parse.Tree, w io.Writer) ([]string, error) { ev.mu.RLock() b, g, m := ev.builtin, ev.global, ev.modules ev.mu.RUnlock() _, autofixes, compileErr := compile(b.static(), g.static(), mapKeys(m), tree, w) return autofixes, compileErr } elvish-0.21.0/pkg/eval/eval_examples_test.go000066400000000000000000000033431465720375400210270ustar00rootroot00000000000000package eval_test import ( "fmt" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) func ExampleEvaler_Eval_usingPortsFromStdFiles() { ev := eval.NewEvaler() // These ports are connected to the process's stdin, stdout and stderr. The // "> " part is the prefix to use for value outputs. ports, cleanup := eval.PortsFromStdFiles("> ") defer cleanup() ev.Eval( parse.Source{Name: "example 1", Code: "echo Hello Elvish!"}, eval.EvalCfg{Ports: ports}) // Value outputs are written with the prefix we specified earlier ev.Eval( parse.Source{Name: "example 2", Code: "put [&foo=bar]"}, eval.EvalCfg{Ports: ports}) // Output: // Hello Elvish! // > [&foo=bar] } func ExampleEvaler_Eval_capturingValueOutputs() { ev := eval.NewEvaler() // The stdout port captures all values written to it, which can be retrieved // with the returned get function. stdout, get, err := eval.ValueCapturePort() if err != nil { panic(err) } ev.Eval( parse.Source{Name: "example 1", Code: "put [&foo=bar] [a b] data"}, eval.EvalCfg{Ports: []*eval.Port{eval.DummyInputPort, stdout, eval.DummyOutputPort}}) values := get() for i, value := range values { fmt.Printf("#%d: %s: %s\n", i, vals.Kind(value), vals.ReprPlain(value)) } // Output: // #0: map: [&foo=bar] // #1: list: [a b] // #2: string: data } func ExampleEvaler_Eval_inspectingGlobal() { ev := eval.NewEvaler() ev.Eval( parse.Source{Name: "example 1", Code: "var map = [&foo=bar]"}, // Omitting the ports connects all of them to "dummy" IO ports. eval.EvalCfg{}) m, ok := ev.Global().Index("map") if !ok { fmt.Println("$map not found") } fmt.Printf("$map: %s: %s\n", vals.Kind(m), vals.ReprPlain(m)) // Output: // $map: map: [&foo=bar] } elvish-0.21.0/pkg/eval/eval_test.elvts000066400000000000000000000021721465720375400176600ustar00rootroot00000000000000////////////////////////////////// # $before-chdir and $after-chdir # ////////////////////////////////// //in-temp-dir ~> use os os:mkdir d var before-dst after-dst set @before-chdir = {|dst| set before-dst = $dst } set @after-chdir = {|dst| set after-dst = $dst } cd d put $before-dst $after-dst ▶ d ▶ d //////// # $pid # //////// ~> > $pid 0 ▶ $true //////////////// # $num-bg-jobs # //////////////// ~> put $num-bg-jobs ▶ 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. ///////// # $args # ///////// ~> put $args ▶ [] ## non-empty ## //args foo bar ~> put $args ▶ [foo bar] //////////////////////// # multiple evaluations # //////////////////////// ~> var x = hello ~> put $x ▶ hello ## variable shadowing ## // Regression test for b.elv.sh/1213 ~> fn f { put old } ~> fn f { put new } ~> f ▶ new ## deleting variable ## // Regression test for b.elv.sh/1213 ~> var x = foo ~> del x ~> put $x Compilation error: variable $x not found [tty]:1:5-6: put $x elvish-0.21.0/pkg/eval/eval_test.go000066400000000000000000000047351465720375400171370ustar00rootroot00000000000000package eval_test import ( "sync" "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/parse" ) func TestEval_AlternativeGlobal(t *testing.T) { ev := NewEvaler() g := BuildNs().AddVar("a", vars.NewReadOnly("")).Ns() err := ev.Eval(parse.Source{Name: "[test]", 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{Name: "[test]", Code: "var a"}, EvalCfg{}) wg.Done() }() go func() { ev.Eval(parse.Source{Name: "[test]", 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: []any{passedArg}, Opts: map[string]any{"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{Name: "[test]", 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.21.0/pkg/eval/evaltest/000077500000000000000000000000001465720375400164405ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/evaltest/non_unix.go000066400000000000000000000000711465720375400206220ustar00rootroot00000000000000//go:build !unix package evaltest const isUNIX = false elvish-0.21.0/pkg/eval/evaltest/test_transcript.go000066400000000000000000000303431465720375400222220ustar00rootroot00000000000000// Package evaltest supports testing the Elvish interpreter and libraries. // // The entrypoint of this package is [TestTranscriptsInFS]. Typical usage looks // like this: // // import ( // "embed" // "src.elv.sh/pkg/eval/evaltest" // ) // // //go:embed *.elv *.elvts // var transcripts embed.FS // // func TestTranscripts(t *testing.T) { // evaltest.TestTranscriptsInFS(t, transcripts) // } // // See [src.elv.sh/pkg/transcript] for how transcript sessions are discovered. // // # Setup functions // // [TestTranscriptsInFS] accepts variadic arguments in (name, f) pairs, where // name must not contain any spaces. Each pair defines a setup function that may // be referred to in the transcripts with the directive "//name". // // The setup function f may take a [*testing.T], [*eval.Evaler] and a string // argument. All of them are optional but must appear in that order. If it takes // a string argument, the directive can be followed by an argument after a space // ("//name argument"), and that argument is passed to f. The argument itself // may contain spaces. // // The following setup functions are predefined: // // - skip-test: Don't run this test. Useful for examples in .d.elv files that // shouldn't be run as tests. // // - in-temp-dir: Run inside a temporary directory. // // - set-env $name $value: Run with the environment variable $name set to // $value. // // - unset-env $name: Run with the environment variable $name unset. // // - eval $code: Evaluate the argument as Elvish code. // // - only-on $cond: Evaluate $cond like a //go:build constraint and only // run the test if the constraint is satisfied. // // The syntax is the same as //go:build constraints, but the set of // supported tags is different and consists of: GOARCH and GOOS values, // "unix", "32bit" and "64bit". // // - deprecation-level $x: Run with deprecation level set to $x. // // These setup functions can then be used in transcripts as directives. By // default, they only apply to the current session; adding a "each:" prefix // makes them apply to descendant sessions too. // // //global-setup // //each:global-setup-2 // // # h1 # // //h1-setup // //each:h1-setup2 // // ## h2 ## // //h2-setup // // // All of globa-setup2, h1-setup2 and h2-setup are run for this session, in // // that // // ~> echo foo // foo // // # ELVISH_TRANSCRIPT_RUN // // The environment variable ELVISH_TRANSCRIPT_RUN may be set to a string // $filename:$lineno. If the location falls within the code lines of an // interaction, the following happens: // // 1. Only the session that the interaction belongs to is run, and only up to // the located interaction. // // 2. If the actual output doesn't match what's in the file, the test fails, // and writes out a machine readable instruction to update the file to match // the actual output. // // As an example, consider the following fragment of foo_test.elvts (with line // numbers): // // 12 ~> echo foo // 13 echo bar // 14 lorem // 15 ipsum // // Running // // env ELVISH_TRANSCRIPT_RUN=foo_test.elvts:12 go test -run TestTranscripts // // will end up with a test failure, with a message like the following (the line // range is left-closed, right-open): // // UPDATE {"fromLine": 14, "toLine": 16, "content": "foo\nbar\n"} // // This mechanism enables editor plugins that can fill or update the output of // transcript tests without requiring user to leave the editor. // // # Deterministic output order // // When Elvish code writes to both the value output and byte output, or to both // stdout and stderr, there's no guarantee which one appears first in the // terminal. // // To make testing easier, this package guarantees a deterministic order in such // cases. package evaltest import ( "bytes" "encoding/json" "fmt" "go/build/constraint" "io/fs" "math" "os" "regexp" "runtime" "strconv" "strings" "testing" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/diff" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/mods" "src.elv.sh/pkg/must" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/transcript" ) // TestTranscriptsInFS extracts all Elvish transcript sessions from .elv and // .elvts files in fsys, and runs each of them as a test. func TestTranscriptsInFS(t *testing.T, fsys fs.FS, setupPairs ...any) { nodes, err := transcript.ParseFromFS(fsys) if err != nil { t.Fatalf("parse transcript sessions: %v", err) } TestTranscriptNodes(t, nodes, setupPairs...) } // TestTranscriptsInFS runs parsed Elvish transcript nodes as tests. func TestTranscriptNodes(t *testing.T, nodes []*transcript.Node, setupPairs ...any) { var run *runCfg if runEnv := os.Getenv("ELVISH_TRANSCRIPT_RUN"); runEnv != "" { filename, lineNo, ok := parseFileNameAndLineNo(runEnv) if !ok { t.Fatalf("can't parse ELVISH_TRANSCRIPT_RUN: %q", runEnv) } var node *transcript.Node for _, n := range nodes { if n.Name == filename { node = n break } } if node == nil { t.Fatalf("can't find file %q", filename) } nodes = []*transcript.Node{node} outputPrefix := "" if strings.HasSuffix(filename, ".elv") { outputPrefix = "# " } run = &runCfg{lineNo, outputPrefix} } testTranscripts(t, buildSetupDirectives(setupPairs), nodes, nil, run) } type runCfg struct { line int outputPrefix string } func parseFileNameAndLineNo(s string) (string, int, bool) { i := strings.LastIndexByte(s, ':') if i == -1 { return "", 0, false } filename, lineNoString := s[:i], s[i+1:] lineNo, err := strconv.Atoi(lineNoString) if err != nil { return "", 0, false } return filename, lineNo, true } var solPattern = regexp.MustCompile("(?m:^)") func testTranscripts(t *testing.T, sd *setupDirectives, nodes []*transcript.Node, setups []setupFunc, run *runCfg) { for _, node := range nodes { if run != nil && !(node.LineFrom <= run.line && run.line < node.LineTo) { continue } t.Run(node.Name, func(t *testing.T) { ev := eval.NewEvaler() mods.AddTo(ev) for _, setup := range setups { setup(t, ev) } var eachSetups []setupFunc for _, directive := range node.Directives { setup, each, err := sd.compile(directive) if err != nil { t.Fatal(err) } setup(t, ev) if each { eachSetups = append(eachSetups, setup) } } for _, interaction := range node.Interactions { if run != nil && interaction.CodeLineFrom > run.line { break } want := interaction.Output got := evalAndCollectOutput(ev, interaction.Code) if want != got { if run == nil { t.Errorf("\n%s\n-want +got:\n%s", interaction.PromptAndCode(), diff.DiffNoHeader(want, got)) } else if interaction.CodeLineFrom <= run.line && run.line < interaction.CodeLineTo { content := got if run.outputPrefix != "" { // Insert output prefix at each SOL, except for the // SOL after the trailing newline. content = solPattern.ReplaceAllLiteralString(strings.TrimSuffix(content, "\n"), run.outputPrefix) + "\n" } correction := struct { FromLine int `json:"fromLine"` ToLine int `json:"toLine"` Content string `json:"content"` }{interaction.OutputLineFrom, interaction.OutputLineTo, content} t.Errorf("UPDATE %s", must.OK1(json.Marshal(correction))) } } } if len(node.Children) > 0 { // TODO: Use slices.Concat when Elvish requires Go 1.22 allSetups := make([]setupFunc, 0, len(setups)+len(eachSetups)) allSetups = append(allSetups, setups...) allSetups = append(allSetups, eachSetups...) testTranscripts(t, sd, node.Children, allSetups, run) } }) } } type ( setupFunc func(*testing.T, *eval.Evaler) argSetupFunc func(*testing.T, *eval.Evaler, string) ) type setupDirectives struct { setupMap map[string]setupFunc argSetupMap map[string]argSetupFunc } func buildSetupDirectives(setupPairs []any) *setupDirectives { if len(setupPairs)%2 != 0 { panic(fmt.Sprintf("variadic arguments must come in pairs, got %d", len(setupPairs))) } setupMap := map[string]setupFunc{ "in-temp-dir": func(t *testing.T, ev *eval.Evaler) { testutil.InTempDir(t) }, "skip-test": func(t *testing.T, _ *eval.Evaler) { t.SkipNow() }, } argSetupMap := map[string]argSetupFunc{ "set-env": func(t *testing.T, ev *eval.Evaler, arg string) { name, value, _ := strings.Cut(arg, " ") testutil.Setenv(t, name, value) }, "unset-env": func(t *testing.T, ev *eval.Evaler, name string) { testutil.Unsetenv(t, name) }, "eval": func(t *testing.T, ev *eval.Evaler, code string) { err := ev.Eval( parse.Source{Name: "[setup]", Code: code}, eval.EvalCfg{Ports: eval.DummyPorts}) if err != nil { t.Fatalf("setup failed: %v\n", err) } }, "only-on": func(t *testing.T, _ *eval.Evaler, arg string) { expr, err := constraint.Parse("//go:build " + arg) if err != nil { t.Fatalf("parse constraint %q: %v", arg, err) } if !expr.Eval(func(tag string) bool { switch tag { case "unix": return isUNIX case "32bit": return math.MaxInt == math.MaxInt32 case "64bit": return math.MaxInt == math.MaxInt64 default: return tag == runtime.GOOS || tag == runtime.GOARCH } }) { t.Skipf("constraint not satisfied: %s", arg) } }, "deprecation-level": func(t *testing.T, _ *eval.Evaler, arg string) { testutil.Set(t, &prog.DeprecationLevel, must.OK1(strconv.Atoi(arg))) }, } for i := 0; i < len(setupPairs); i += 2 { name := setupPairs[i].(string) if setupMap[name] != nil || argSetupMap[name] != nil { panic(fmt.Sprintf("there's already a setup functions named %s", name)) } switch f := setupPairs[i+1].(type) { case func(): setupMap[name] = func(_ *testing.T, _ *eval.Evaler) { f() } case func(*testing.T): setupMap[name] = func(t *testing.T, ev *eval.Evaler) { f(t) } case func(*eval.Evaler): setupMap[name] = func(t *testing.T, ev *eval.Evaler) { f(ev) } case func(*testing.T, *eval.Evaler): setupMap[name] = f case func(string): argSetupMap[name] = func(_ *testing.T, _ *eval.Evaler, s string) { f(s) } case func(*testing.T, string): argSetupMap[name] = func(t *testing.T, _ *eval.Evaler, s string) { f(t, s) } case func(*eval.Evaler, string): argSetupMap[name] = func(_ *testing.T, ev *eval.Evaler, s string) { f(ev, s) } case func(*testing.T, *eval.Evaler, string): argSetupMap[name] = f default: panic(fmt.Sprintf("unsupported setup function type: %T", f)) } } return &setupDirectives{setupMap, argSetupMap} } func (sd *setupDirectives) compile(directive string) (f setupFunc, each bool, err error) { cutDirective := directive if s, ok := strings.CutPrefix(directive, "each:"); ok { cutDirective = s each = true } name, arg, _ := strings.Cut(cutDirective, " ") if f, ok := sd.setupMap[name]; ok { if arg != "" { return nil, false, fmt.Errorf("setup function %q doesn't support arguments", name) } return f, each, nil } else if f, ok := sd.argSetupMap[name]; ok { return func(t *testing.T, ev *eval.Evaler) { f(t, ev, arg) }, each, nil } else { return nil, false, fmt.Errorf("unknown setup function %q in directive %q", name, directive) } } var valuePrefix = "▶ " func evalAndCollectOutput(ev *eval.Evaler, code string) string { port1, collect1 := must.OK2(eval.CapturePort()) port2, collect2 := must.OK2(eval.CapturePort()) ports := []*eval.Port{eval.DummyInputPort, port1, port2} ctx, done := eval.ListenInterrupts() err := ev.Eval( parse.Source{Name: "[tty]", Code: code}, eval.EvalCfg{Ports: ports, Interrupts: ctx}) done() values, stdout := collect1() _, stderr := collect2() var sb strings.Builder for _, value := range values { sb.WriteString(valuePrefix + vals.ReprPlain(value) + "\n") } sb.Write(normalizeLineEnding(stripSGR(stdout))) sb.Write(normalizeLineEnding(stripSGR(stderr))) if err != nil { if shower, ok := err.(diag.Shower); ok { sb.WriteString(stripSGRString(shower.Show(""))) } else { sb.WriteString(err.Error()) } sb.WriteByte('\n') } return sb.String() } var sgrPattern = regexp.MustCompile("\033\\[[0-9;]*m") func stripSGR(bs []byte) []byte { return sgrPattern.ReplaceAllLiteral(bs, nil) } func stripSGRString(s string) string { return sgrPattern.ReplaceAllLiteralString(s, "") } func normalizeLineEnding(bs []byte) []byte { return bytes.ReplaceAll(bs, []byte("\r\n"), []byte("\n")) } elvish-0.21.0/pkg/eval/evaltest/unix.go000066400000000000000000000000671465720375400177550ustar00rootroot00000000000000//go:build unix package evaltest const isUNIX = true elvish-0.21.0/pkg/eval/exception.go000066400000000000000000000221431465720375400171400ustar00rootroot00000000000000package 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 (*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 } var _ vals.PseudoMap = &exception{} // 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() } var ( exceptionCauseStartMarker = "\033[31;1m" exceptionCauseEndMarker = "\033[m" ) // 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 = exceptionCauseStartMarker + exc.reason.Error() + exceptionCauseEndMarker } fmt.Fprintf(buf, "Exception: %s", causeDescription) if exc.stackTrace != nil { 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 "[^exception &reason=" + vals.Repr(exc.reason, indent+1) + " &stack-trace=<...>]" } // Equal compares by address. func (exc *exception) Equal(rhs any) 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 } func (f excFields) StackTrace() *StackTrace { return f.e.stackTrace } // PipelineError represents the errors of pipelines, in which multiple commands // may error. type PipelineError struct { Errors []Exception } var _ vals.PseudoMap = PipelineError{} // 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) Kind() string { return "pipeline-error" } 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.Conj(exc) } return li } // Flow is a special type of error used for control flows. type Flow uint var _ vals.PseudoMap = Flow(0) // 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) Kind() string { return "flow-error" } 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 } var _ vals.PseudoMap = ExternalCmdExit{} // 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) Kind() string { return "external-cmd-error" } 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.21.0/pkg/eval/exception_test.elvts000066400000000000000000000013761465720375400207340ustar00rootroot00000000000000////////////////////// # Flow introspection # ////////////////////// ~> put ?(return)[reason][type name] ▶ flow ▶ return ///////////////////////////////// # ExternalCmdExit introspection # ///////////////////////////////// ## Unix ## //only-on unix ~> put ?(false)[reason][type exit-status] ▶ external-cmd/exited ▶ 1 ## Windows ## //only-on windows ~> put ?(cmd /c exit 1)[reason][type exit-status] ▶ external-cmd/exited ▶ 1 // TODO: Test killed and stopped commands /////////////////////////////// # PipelineError introspection # /////////////////////////////// ~> put ?(fail 1 | fail 2)[reason][type] ▶ pipeline ~> count ?(fail 1 | fail 2)[reason][exceptions] ▶ (num 2) ~> put ?(fail 1 | fail 2)[reason][exceptions][0][reason][type] ▶ fail elvish-0.21.0/pkg/eval/exception_test.go000066400000000000000000000055411465720375400202020ustar00rootroot00000000000000package eval_test import ( "errors" "reflect" "testing" "unsafe" "src.elv.sh/pkg/diag" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" "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, Reason, Args(err).Rets(err), 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", "stack-trace"). Index("reason", err). IndexError("stack", vals.NoSuchKey("stack")). Repr("[^exception &reason=[^fail-error &content=error &type=fail] &stack-trace=<...>]") vals.TestValue(t, OK). Kind("exception"). Bool(true). Repr("$ok") } func TestException_Show(t *testing.T) { for _, p := range []*string{ ExceptionCauseStartMarker, ExceptionCauseEndMarker, &diag.ContextBodyStartMarker, &diag.ContextBodyEndMarker} { testutil.Set(t, p, "") } tt.Test(t, Exception.Show, It("supports exceptions with one traceback frame"). Args(makeException( errors.New("internal error"), diag.NewContext("a.elv", "echo bad", diag.Ranging{From: 5, To: 8})), ""). Rets(Dedent(` Exception: internal error a.elv:1:6-8: echo bad`)), It("supports exceptions with multiple traceback frames"). Args(makeException( errors.New("internal error"), diag.NewContext("a.elv", "echo bad", diag.Ranging{From: 5, To: 8}), diag.NewContext("b.elv", "use foo", diag.Ranging{From: 0, To: 7})), ""). Rets(Dedent(` Exception: internal error a.elv:1:6-8: echo bad b.elv:1:1-7: use foo`)), It("supports traceback frames with multi-line body text"). Args(makeException( errors.New("internal error"), diag.NewContext("a.elv", "echo ba\nd", diag.Ranging{From: 5, To: 9})), ""). Rets(Dedent(` Exception: internal error a.elv:1:6-2:1: echo ba d`)), ) } 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 TestErrorMethods(t *testing.T) { tt.Test(t, error.Error, Args(makeException(errors.New("err"))).Rets("err"), Args(MakePipelineError([]Exception{ makeException(errors.New("err1")), makeException(errors.New("err2"))})).Rets("(err1 | err2)"), Args(Return).Rets("return"), Args(Break).Rets("break"), Args(Continue).Rets("continue"), Args(Flow(1000)).Rets("!(BAD FLOW: 1000)"), ) } elvish-0.21.0/pkg/eval/exception_unix_test.go000066400000000000000000000024611465720375400212430ustar00rootroot00000000000000//go:build unix 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, error.Error, Args(ExternalCmdExit{0x0, "ls", 1}).Rets("ls exited with 0"), 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"). Args(ExternalCmdExit{0x2, "ls", 1}).Rets("ls killed by signal "+syscall.SIGINT.String()), // 0x80 + signal for core dumped Args(ExternalCmdExit{0x82, "ls", 1}).Rets("ls killed by signal "+syscall.SIGINT.String()+" (core dumped)"), // 0x7f + signal<<8 for stopped Args(ExternalCmdExit{0x27f, "ls", 1}).Rets("ls stopped by signal "+syscall.SIGINT.String()+" (pid=1)"), ) if runtime.GOOS == "linux" { tt.Test(t, error.Error, // 0x057f + cause<<16 for trapped. SIGTRAP is 5 on all Unix'es but have // different string representations on different OSes. 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. Args(ExternalCmdExit{0xff, "ls", 1}).Rets("ls has unknown WaitStatus 255"), ) } } elvish-0.21.0/pkg/eval/external_cmd.go000066400000000000000000000066401465720375400176130ustar00rootroot00000000000000package eval import ( "errors" "os" "os/exec" "path/filepath" "runtime" "strings" "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 any) 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 []any, opts map[string]any) 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 } fm.Deprecate("implicit cd is deprecated; use cd or location mode instead", fm.traceback.Head, 21) 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 } if runtime.GOOS == "windows" && !filepath.IsAbs(path) { // For some reason, Windows's CreateProcess API doesn't like forward // slashes in relative paths: ".\foo.bat" works but "./foo.bat" results // in an error message that "'.' is not recognized as an internal or // external command, operable program or batch file." // // There seems to be no good reason for this behavior, so we work around // it by replacing forward slashes with backslashes. PowerShell seems to // be something similar to support "./foo.bat". path = strings.ReplaceAll(path, "/", "\\") } 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 && readerGone.Load() { return errs.ReaderGone{} } } return NewExternalCmdExit(e.Name, state.Sys().(syscall.WaitStatus), proc.Pid) } elvish-0.21.0/pkg/eval/external_cmd_unix.go000066400000000000000000000001701465720375400206460ustar00rootroot00000000000000//go:build unix package eval import "syscall" func isSIGPIPE(s syscall.Signal) bool { return s == syscall.SIGPIPE } elvish-0.21.0/pkg/eval/external_cmd_unix_internal_test.go000066400000000000000000000047221465720375400236100ustar00rootroot00000000000000//go:build unix 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 := testutil.InTempDir(t) testutil.ApplyDir(testutil.Dir{ "bin": testutil.Dir{ "elvish": testutil.File{Perm: 0755}, "cat": testutil.File{Perm: 0755}, }, }) testutil.Setenv(t, "PATH", dir+"/bin") testutil.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() testutil.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.21.0/pkg/eval/external_cmd_windows.go000066400000000000000000000001721465720375400213570ustar00rootroot00000000000000package eval import "syscall" func isSIGPIPE(s syscall.Signal) bool { // Windows doesn't have SIGPIPE. return false } elvish-0.21.0/pkg/eval/frame.go000066400000000000000000000172761465720375400162470ustar00rootroot00000000000000package eval import ( "bufio" "context" "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 src parse.Source local, up *Ns defers *[]func(*Frame) Exception // The godoc of the context package states: // // > Do not store Contexts inside a struct type; instead, pass a Context // > explicitly to each function that needs it. // // However, that advice is considered by many to be overly aggressive // (https://github.com/golang/go/issues/22602). The Frame struct doesn't fit // the "parameter struct" definition in that discussion, but it is itself is // a "context struct". Storing a Context inside it seems fine. ctx context.Context 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), nil, fm.ctx, fm.ports, traceback, fm.background} op, _, err := compile(fm.Evaler.Builtin().static(), local.static(), nil, 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 any { 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 } // Port returns port i. If the port doesn't exist, it returns nil // // This is a low-level construct that shouldn't be used for writing output; for // that purpose, use [(*Frame).ValueOutput] and [(*Frame).ByteOutput] instead. func (fm *Frame) Port(i int) *Port { if i >= len(fm.ports) { return nil } return fm.ports[i] } // IterateInputs calls the passed function for each input element. func (fm *Frame) IterateInputs(f func(any)) { var wg sync.WaitGroup inputs := make(chan any) 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<- any) { 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 } } } // Context returns a Context associated with the Frame. func (fm *Frame) Context() context.Context { return fm.ctx } // Canceled reports whether the Context of the Frame has been canceled. func (fm *Frame) Canceled() bool { select { case <-fm.ctx.Done(): return true default: return false } } // Fork returns a modified copy of fm. The ports are forked, and the name is // changed to the given value. Other fields are copied shallowly. func (fm *Frame) Fork() *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.src, fm.local, fm.up, fm.defers, fm.ctx, newPorts, fm.traceback, fm.background, } } // A shorthand for forking a frame and setting the output port. func (fm *Frame) forkWithOutput(p *Port) *Frame { newFm := fm.Fork() 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) ([]any, error) { outPort, collect, err := ValueCapturePort() if err != nil { return nil, err } err = f(fm.forkWithOutput(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 any), bCb func(*os.File)) error { outPort, done, err := PipePort(vCb, bCb) if err != nil { return err } err = f(fm.forkWithOutput(outPort)) done() return err } func (fm *Frame) addTraceback(r diag.Ranger) *StackTrace { return &StackTrace{ Head: diag.NewContext(fm.src.Name, fm.src.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: if _, ok := e.(errs.SetReadOnlyVar); ok { r := r.Range() e = errs.SetReadOnlyVar{VarName: fm.src.Code[r.From:r.To]} } ctx := diag.NewContext(fm.src.Name, fm.src.Code, r) 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 ...any) 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 { ctx = fm.traceback.Head } if fm.Evaler.registerDeprecation(deprecation{ctx.Name, ctx.Ranging, msg}) { err := diag.Error[deprecationTag]{Message: msg, Context: *ctx} fm.ErrorFile().WriteString(err.Show("") + "\n") } } func (fm *Frame) addDefer(f func(*Frame) Exception) { *fm.defers = append(*fm.defers, f) } func (fm *Frame) runDefers() Exception { var exc Exception defers := *fm.defers for i := len(defers) - 1; i >= 0; i-- { exc2 := defers[i](fm) // TODO: Combine exc and exc2 if both are not nil if exc2 != nil && exc == nil { exc = exc2 } } return exc } elvish-0.21.0/pkg/eval/fuzz_test.go000066400000000000000000000004341465720375400171760ustar00rootroot00000000000000package eval import ( "testing" "src.elv.sh/pkg/parse" ) func FuzzCheck(f *testing.F) { f.Add("echo") f.Add("put $x") f.Add("put foo bar | each {|x| echo $x }") f.Fuzz(func(t *testing.T, code string) { NewEvaler().Check(parse.Source{Name: "[fuzz]", Code: code}, nil) }) } elvish-0.21.0/pkg/eval/generic_utils.go000066400000000000000000000005401465720375400177730ustar00rootroot00000000000000package eval // Some generic utils that should appear in the standard library soon. func mapKeys[K comparable, V any](m map[K]V) []K { ks := make([]K, 0, len(m)) for k := range m { ks = append(ks, k) } return ks } func sliceContains[T comparable](xs []T, y T) bool { for _, x := range xs { if x == y { return true } } return false } elvish-0.21.0/pkg/eval/glob.go000066400000000000000000000146371465720375400160760ustar00rootroot00000000000000package eval import ( "context" "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 any) (any, 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.ReprPlain(modifierv)) } err := gp.addMatcher(matcher) return gp, err } return gp, nil } func (gp globPattern) Concat(v any) (any, error) { switch rhs := v.(type) { case string: var segs []glob.Segment segs = append(segs, gp.Segments...) segs = append(segs, stringToSegments(rhs)...) return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags, Buts: gp.Buts, TypeCb: gp.TypeCb}, 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 any) (any, 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(ctx context.Context, gp globPattern) ([]any, error) { but := make(map[string]struct{}) for _, s := range gp.Buts { but[s] = struct{}{} } vs := make([]any, 0) if !gp.Glob(func(pathInfo glob.PathInfo) bool { select { case <-ctx.Done(): 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.21.0/pkg/eval/glob_test.elvts000066400000000000000000000055331465720375400176600ustar00rootroot00000000000000//////////// # globbing # //////////// //each:in-temp-dir ## simple patterns ## ~> use os put z z2 | each $os:mkdir~ put bar foo ipsum lorem | each {|x| echo > $x} ~> put * ▶ bar ▶ foo ▶ ipsum ▶ lorem ▶ z ▶ z2 ~> put z* ▶ z ▶ z2 ~> put ? ▶ z ~> put ????m ▶ ipsum ▶ lorem ## glob applies after brace ## ~> put xy.u xy.v xy.w xz.w | each {|x| echo > $x} ~> put x*.{u v w} ▶ xy.u ▶ xy.v ▶ xy.w ▶ xz.w ## recursive patterns ## ~> use os put 1 1/2 1/2/3 | each $os:mkdir~ put a.go 1/a.go 1/2/3/a.go | each {|x| echo > $x} ~> put ** ▶ 1/2/3/a.go ▶ 1/2/3 ▶ 1/2 ▶ 1/a.go ▶ 1 ▶ a.go ~> put **.go ▶ 1/2/3/a.go ▶ 1/a.go ▶ a.go ~> put 1**.go ▶ 1/2/3/a.go ▶ 1/a.go ## no match ## ~> put a/b/nonexistent* Exception: wildcard has no match [tty]:1:5-20: put a/b/nonexistent* ~> put a/b/nonexistent*[nomatch-ok] ## hidden files ## ~> use os put d .d | each $os:mkdir~ put a .a d/a d/.a .d/a .d/.a | each {|x| echo > $x} ~> put * ▶ a ▶ d ~> put *[match-hidden] ▶ .a ▶ .d ▶ a ▶ d ~> put *[match-hidden]/* ▶ .d/a ▶ d/a ~> put */*[match-hidden] ▶ d/.a ▶ d/a ~> put *[match-hidden]/*[match-hidden] ▶ .d/.a ▶ .d/a ▶ d/.a ▶ d/a ## rune matchers ## ~> put a1 a2 b1 c1 ipsum lorem | each {|x| echo > $x} ~> put *[letter] ▶ ipsum ▶ lorem ~> put ?[set:ab]* ▶ a1 ▶ a2 ▶ b1 ~> put ?[range:a-c]* ▶ a1 ▶ a2 ▶ b1 ▶ c1 ~> put ?[range:a~c]* ▶ a1 ▶ a2 ▶ b1 ~> put *[range:a-z] ▶ ipsum ▶ lorem ~> put *[range:a-zz] Exception: bad range modifier: a-zz [tty]:1:5-17: put *[range:a-zz] ~> put *[range:foo] Exception: bad range modifier: foo [tty]:1:5-16: put *[range:foo] ## but ## ~> put bar foo ipsum lorem | each {|x| echo > $x} ~> put *[but:ipsum] ▶ bar ▶ foo ▶ lorem // Nonexistent files can also be excluded ~> put *[but:foobar][but:ipsum] ▶ bar ▶ foo ▶ lorem ## type ## ~> use os put d1 d2 .d b b/c | each $os:mkdir~ put bar foo ipsum lorem d1/f1 d2/fm | each {|x| echo > $x} ~> put **[type:dir] ▶ b/c ▶ b ▶ d1 ▶ d2 ~> put **[type:regular]m ▶ d2/fm ▶ ipsum ▶ lorem ~> put **[type:regular]f* ▶ d1/f1 ▶ d2/fm ▶ foo ~> put **f*[type:regular] ▶ d1/f1 ▶ d2/fm ▶ foo ~> put *[type:dir][type:regular] Exception: only one type modifier allowed [tty]:1:5-29: put *[type:dir][type:regular] ~> put **[type:dir]f*[type:regular] Exception: only one type modifier allowed [tty]:1:5-32: put **[type:dir]f*[type:regular] ~> put **[type:unknown] Exception: unknown type modifier [tty]:1:5-20: put **[type:unknown] ## bad operations ## ~> put *[[]] Exception: modifier must be string [tty]:1:5-9: put *[[]] ~> put *[bad-mod] Exception: unknown modifier bad-mod [tty]:1:5-14: put *[bad-mod] ~> put *{ } Exception: cannot concatenate glob-pattern and fn [tty]:1:5-8: put *{ } ~> put { }* Exception: cannot concatenate fn and glob-pattern [tty]:1:5-8: put { }* elvish-0.21.0/pkg/eval/go_fn.go000066400000000000000000000171101465720375400162300ustar00rootroot00000000000000package 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") ) // WrongArgType is thrown when calling a native function with an argument of the // wrong type. type WrongArgType struct { argNum int typeError error } // Error implements the error interface. func (e WrongArgType) Error() string { return fmt.Sprintf("wrong type for arg #%d: %v", e.argNum, e.typeError) } // Unwrap returns the wrapped type error. func (e WrongArgType) Unwrap() error { return e.typeError } type goFn struct { name string impl any // 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(any)) 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 any) 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 any) 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 []any, opts map[string]any) 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 WrongArgType{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(any)) { // CanIterate(iterable) is true _ = vals.Iterate(iterable, func(v any) 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.21.0/pkg/eval/go_fn_internal_test.go000066400000000000000000000005521465720375400211650ustar00rootroot00000000000000package 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.21.0/pkg/eval/go_fn_test.elvts000066400000000000000000000051401465720375400200170ustar00rootroot00000000000000//each:go-fns-mod-in-global /////////// # nullary # /////////// ~> go-fns:nullary //////////// # argument # //////////// ~> go-fns:takes-two-strings lorem ipsum a = "lorem", b = "ipsum" ~> go-fns:takes-variadic-strings lorem ipsum args = ["lorem" "ipsum"] ~> go-fns:takes-string-and-variadic-strings lorem ipsum first = "lorem", more = ["ipsum"] ~> go-fns:takes-int-float64 314 1.25 i = 314, f = 1.25 ## wrong number of arguments ## ~> go-fns:nullary foo Exception: arity mismatch: arguments must be 0 values, but is 1 value [tty]:1:1-18: go-fns:nullary foo ~> go-fns:takes-two-strings foo Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-28: go-fns:takes-two-strings foo ~> go-fns:takes-string-and-variadic-strings Exception: arity mismatch: arguments must be 1 or more values, but is 0 values [tty]:1:1-40: go-fns:takes-string-and-variadic-strings ## wrong argument type ## ~> go-fns:takes-two-strings foo [] Exception: wrong type for arg #1: wrong type: need string, got list [tty]:1:1-31: go-fns:takes-two-strings foo [] ~> go-fns:takes-int-float64 foo 1.2 Exception: wrong type for arg #0: cannot parse as integer: foo [tty]:1:1-32: go-fns:takes-int-float64 foo 1.2 ////////// # inputs # ////////// ~> go-fns:takes-input [foo bar] input: foo input: bar ~> put foo bar | go-fns:takes-input input: foo input: bar /////////// # options # /////////// ## parsed options ## ~> go-fns:takes-options &foo=lorem opts = eval_test.someOptions{Foo:"lorem", Bar:"default"} ~> go-fns:takes-options &foo=lorem &bar=ipsum opts = eval_test.someOptions{Foo:"lorem", Bar:"ipsum"} ## RawOptions ## ~> go-fns:takes-raw-options &foo=ipsum opts = eval.RawOptions{"foo":"ipsum"} ~> go-fns:takes-raw-options &foo=ipsum &bar=ipsum opts = eval.RawOptions{"bar":"ipsum", "foo":"ipsum"} ## errors ## ~> go-fns:nullary &foo=lorem Exception: function does not accept any options [tty]:1:1-25: go-fns:nullary &foo=lorem // Regression tests for b.elv.sh/958. ~> go-fns:takes-options &bad=value Exception: unknown option: bad [tty]:1:1-31: go-fns:takes-options &bad=value ///////////////// # return values # ///////////////// ~> go-fns:returns-string ▶ 'a string' ~> go-fns:returns-int ▶ (num 233) ~> go-fns:returns-small-big-int ▶ (num 233) ~> go-fns:returns-slice ▶ foo ▶ bar ~> go-fns:returns-array ▶ foo ▶ bar // Named type with underlying slice type is not treated as slices ~> go-fns:returns-named-slice-type ▶ //////////////// # error return # //////////////// ~> go-fns:returns-non-nil-error Exception: bad [tty]:1:1-28: go-fns:returns-non-nil-error ~> go-fns:returns-nil-error elvish-0.21.0/pkg/eval/go_fn_test.go000066400000000000000000000032461465720375400172740ustar00rootroot00000000000000package eval_test import ( "errors" "fmt" "math/big" . "src.elv.sh/pkg/eval" ) type someOptions struct { Foo string Bar string } func (o *someOptions) SetDefaultOptions() { o.Bar = "default" } type namedSlice []string var goFnsMod = BuildNs().AddGoFns(map[string]any{ "nullary": func() {}, "takes-two-strings": func(fm *Frame, a, b string) { fmt.Fprintf(fm.ByteOutput(), "a = %q, b = %q\n", a, b) }, "takes-variadic-strings": func(fm *Frame, args ...string) { fmt.Fprintf(fm.ByteOutput(), "args = %q\n", args) }, "takes-string-and-variadic-strings": func(fm *Frame, first string, more ...string) { fmt.Fprintf(fm.ByteOutput(), "first = %q, more = %q\n", first, more) }, "takes-int-float64": func(fm *Frame, i int, f float64) { fmt.Fprintf(fm.ByteOutput(), "i = %v, f = %v\n", i, f) }, "takes-input": func(fm *Frame, i Inputs) { i(func(x any) { fmt.Fprintf(fm.ByteOutput(), "input: %v\n", x) }) }, "takes-options": func(fm *Frame, opts someOptions) { fmt.Fprintf(fm.ByteOutput(), "opts = %#v\n", opts) }, "takes-raw-options": func(fm *Frame, opts RawOptions) { fmt.Fprintf(fm.ByteOutput(), "opts = %#v\n", opts) }, "returns-string": func() string { return "a string" }, "returns-int": func() int { return 233 }, "returns-small-big-int": func() *big.Int { return big.NewInt(233) }, "returns-slice": func() []string { return []string{"foo", "bar"} }, "returns-array": func() [2]string { return [2]string{"foo", "bar"} }, "returns-named-slice-type": func() namedSlice { return namedSlice{"foo", "bar"} }, "returns-non-nil-error": func() error { return errors.New("bad") }, "returns-nil-error": func() error { return nil }, }) elvish-0.21.0/pkg/eval/hook.go000066400000000000000000000021561465720375400161040ustar00rootroot00000000000000package eval import ( "fmt" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/eval/vals" ) // CallHook runs all the functions in the list "hook", with "args". // // If "evalCfg" is not specified, the standard files will be used for IO. // // TODO: Eventually all callers should supply evalCfg. In general it's not // correct to use standard files: // // - Chdir hooks should use the frame from which the chdir is triggered. // - Editor lifecycle hooks should use the editor's TTY. func CallHook(ev *Evaler, evalCfg *EvalCfg, name string, hook vals.List, args ...any) { if hook.Len() == 0 { return } if evalCfg == nil { ports, cleanup := PortsFromStdFiles(ev.ValuePrefix()) defer cleanup() evalCfg = &EvalCfg{Ports: ports} } callCfg := CallCfg{Args: args, From: "[hook " + name + "]"} i := -1 stderr := evalCfg.Ports[2].File for it := hook.Iterator(); it.HasElem(); it.Next() { i++ fn, ok := it.Elem().(Callable) if !ok { diag.ShowError(stderr, fmt.Errorf("hook %s[%d] must be callable", name, i)) continue } err := ev.Call(fn, callCfg, *evalCfg) if err != nil { diag.ShowError(stderr, err) } } } elvish-0.21.0/pkg/eval/hook_test.elvts000066400000000000000000000007611465720375400176730ustar00rootroot00000000000000//each:call-hook-in-global ~> call-hook test-hook [{ echo hook1 } { echo hook2 }] hook1 hook2 // Arguments ~> call-hook test-hook [{|x| echo hook$x }] foo hookfoo // Invalid hook list ~> call-hook test-hook [not-a-fn] hook test-hook[0] must be callable // Exception thrown from hook prints the exception to port 2, rather than being // propagated ~> call-hook test-hook [{ fail bad }] echo after call-hook >&2 Exception: bad [tty]:1:24-32: call-hook test-hook [{ fail bad }] after call-hook elvish-0.21.0/pkg/eval/interrupts.go000066400000000000000000000013721465720375400173620ustar00rootroot00000000000000package eval import ( "context" "errors" "os" "os/signal" "syscall" ) // ErrInterrupted is thrown when the execution is interrupted by a signal. var ErrInterrupted = errors.New("interrupted") // ListenInterrupts returns a Context that is canceled when SIGINT or SIGQUIT // has been received by the process. It also returns a function to cancel the // Context, which should be called when it is no longer needed. func ListenInterrupts() (context.Context, func()) { ctx, cancel := context.WithCancel(context.Background()) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGQUIT) go func() { select { case <-sigCh: cancel() case <-ctx.Done(): } signal.Stop(sigCh) }() return ctx, func() { cancel() } } elvish-0.21.0/pkg/eval/node_utils.go000066400000000000000000000066521465720375400173160ustar00rootroot00000000000000package 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 { if len(n.Indexings) == 0 { cp.errorpfPartial(n, "%v", err) } else { cp.errorpf(n, "%v", err) } } return s } type argsGetter struct { cp *compiler fn *parse.Form ok bool n int } func getArgs(cp *compiler, fn *parse.Form) *argsGetter { return &argsGetter{cp, fn, true, 0} } func (ag *argsGetter) errorpf(r diag.Ranger, format string, args ...any) { if ag.ok { ag.cp.errorpf(r, format, args...) ag.ok = false } } func (ag *argsGetter) errorpfPartial(r diag.Ranger, format string, args ...any) { if ag.ok { ag.cp.errorpfPartial(r, format, args...) ag.ok = false } } func (ag *argsGetter) get(i int, what string) *argAsserter { if ag.n < i+1 { ag.n = i + 1 } if i >= len(ag.fn.Args) { ag.errorpfPartial(diag.PointRanging(ag.fn.To), "need %s", what) return &argAsserter{ag, what, nil} } return &argAsserter{ag, what, ag.fn.Args[i]} } func (ag *argsGetter) has(i int) bool { return i < len(ag.fn.Args) } func (ag *argsGetter) hasKeyword(i int, kw string) bool { if i < len(ag.fn.Args) { s, ok := cmpd.StringLiteral(ag.fn.Args[i]) return ok && s == kw } return false } func (ag *argsGetter) optionalKeywordBody(i int, kw string) *parse.Primary { if ag.has(i+1) && ag.hasKeyword(i, kw) { return ag.get(i+1, kw+" body").thunk() } return nil } func (ag *argsGetter) finish() bool { if ag.n < len(ag.fn.Args) { // In general, the "superfluous" argument may actually be incomplete // optional keywords (like "el" in an "if" form), hence this calls // errorpfPartial instead of errorpf. // // TODO: Make this more accurate. We should have all the information we // need to say that whether this is partial or not, but in order to // retain that information we'll probably need a more declarative API // for parsing special forms. ag.errorpfPartial( diag.Ranging{From: ag.fn.Args[ag.n].Range().From, To: ag.fn.To}, "superfluous arguments") } return ag.ok } type argAsserter struct { ag *argsGetter what string node *parse.Compound } func (aa *argAsserter) any() *parse.Compound { return aa.node } func (aa *argAsserter) stringLiteral() string { if aa.node == nil { return "" } s, err := cmpd.StringLiteralOrError(aa.node, aa.what) if err != nil { aa.ag.errorpf(aa.node, "%v", err) return "" } return s } func (aa *argAsserter) lambda() *parse.Primary { if aa.node == nil { return nil } lambda, ok := cmpd.Lambda(aa.node) if !ok { if p, ok := cmpd.Primary(aa.node); ok && p.Type == parse.Braced { // If we have seen just a "{", the parser will parse a braced // expression, but this could as well be the start of a lambda. aa.ag.errorpfPartial(aa.node, "%s must be lambda, found %s", aa.what, cmpd.Shape(aa.node)) } else { aa.ag.errorpf(aa.node, "%s must be lambda, found %s", aa.what, cmpd.Shape(aa.node)) } return nil } return lambda } func (aa *argAsserter) thunk() *parse.Primary { lambda := aa.lambda() if lambda == nil { return nil } if len(lambda.Elements) > 0 { aa.ag.errorpf(lambda, "%s must not have arguments", aa.what) return nil } if len(lambda.MapPairs) > 0 { aa.ag.errorpf(lambda, "%s must not have options", aa.what) return nil } return lambda } elvish-0.21.0/pkg/eval/ns.go000066400000000000000000000172251465720375400155670ustar00rootroot00000000000000package 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 := ns2.clone() 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 } func (ns *Ns) clone() *Ns { return &Ns{ append([]vars.Var(nil), ns.slots...), append([]staticVarInfo(nil), ns.infos...)} } // 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 any) 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 IndexString. func (ns *Ns) Index(k any) (any, bool) { if ks, ok := k.(string); ok { variable := ns.IndexString(ks) if variable == nil { return nil, false } return variable.Get(), true } return nil, false } // IndexString 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(any) 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("") } // BuildNsNamed 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)} } // AddVar 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 any) 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]any) 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.21.0/pkg/eval/ns_test.elvts000066400000000000000000000007361465720375400173550ustar00rootroot00000000000000////// # ns # ////// ~> kind-of (ns [&]) ▶ ns ## equality ## // an Ns is only equal to itself ## ~> var ns = (ns [&]) eq $ns $ns ▶ $true ~> eq (ns [&]) (ns [&]) ▶ $false ~> eq (ns [&]) [&] ▶ $false ## access ## ~> var ns: = (ns [&a=b &x=y]) put $ns:a ▶ b ~> var ns: = (ns [&a=b &x=y]) put $ns:[a] ▶ b ## keys ## ~> keys (ns [&a=b &x=y]) | order ▶ a ▶ x ## has-key ## ~> has-key (ns [&a=b &x=y]) a ▶ $true ~> has-key (ns [&a=b &x=y]) b ▶ $false elvish-0.21.0/pkg/eval/options.go000066400000000000000000000024211465720375400166320ustar00rootroot00000000000000package eval import ( "reflect" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) // UnknownOption is thrown by a native function when called with an unknown option. type UnknownOption struct { OptName string } // Error implements the error interface. func (e UnknownOption) Error() string { return "unknown option: " + parse.Quote(e.OptName) } // 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]any // 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. Options that don't have corresponding fields in the struct causes // an error. // // Similar to vals.ScanMapToGo, but requires rawOpts to contain a subset of keys // supported by the struct. func scanOptions(rawOpts RawOptions, ptr any) error { _, keyIdx := vals.StructFieldsInfo(reflect.TypeOf(ptr).Elem()) structValue := reflect.ValueOf(ptr).Elem() for k, v := range rawOpts { fieldIdx, ok := keyIdx[k] if !ok { return UnknownOption{k} } err := vals.ScanToGo(v, structValue.Field(fieldIdx).Addr().Interface()) if err != nil { return err } } return nil } elvish-0.21.0/pkg/eval/options_test.go000066400000000000000000000014651465720375400177000ustar00rootroot00000000000000package eval import ( "reflect" "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args type opts struct { Foo string bar int } // Equal is required by cmp.Diff, since opts contains unexported fields. func (o opts) Equal(p opts) bool { return o == p } func TestScanOptions(t *testing.T) { // A wrapper of ScanOptions, to make it easier to test wrapper := func(src RawOptions, dstInit any) (any, error) { ptr := reflect.New(reflect.TypeOf(dstInit)) ptr.Elem().Set(reflect.ValueOf(dstInit)) err := scanOptions(src, ptr.Interface()) return ptr.Elem().Interface(), err } tt.Test(t, tt.Fn(wrapper).Named("scanOptions"), Args(RawOptions{"foo": "lorem ipsum"}, opts{}). Rets(opts{Foo: "lorem ipsum"}, nil), Args(RawOptions{"bar": 20}, opts{bar: 10}). Rets(opts{bar: 10}, UnknownOption{"bar"}), ) } elvish-0.21.0/pkg/eval/plugin.go000066400000000000000000000001171465720375400164350ustar00rootroot00000000000000//go:build !gccgo package eval import "plugin" var pluginOpen = plugin.Open elvish-0.21.0/pkg/eval/plugin_gccgo.go000066400000000000000000000005221465720375400175770ustar00rootroot00000000000000//go: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) (any, error) { return nil, errPluginNotImplemented } elvish-0.21.0/pkg/eval/port.go000066400000000000000000000213601465720375400161260ustar00rootroot00000000000000package eval import ( "bufio" "errors" "fmt" "io" "os" "sync" "sync/atomic" "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 any 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, sendError is // populated and sendStop 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 true 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 *atomic.Bool } // ErrPortDoesNotSupportValueOutput is thrown when writing to a port that does // not support value output. var ErrPortDoesNotSupportValueOutput = errors.New("port does not support 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 any { ch := make(chan any) close(ch) return ch } func getBlackholeChan() chan any { ch := make(chan any) 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 any), bCb func(*os.File)) (*Port, func(), error) { r, w, err := os.Pipe() if err != nil { return nil, nil, err } ch := make(chan any, 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 // saved separately. It also returns a function to call to obtain the captured // output. func CapturePort() (*Port, func() ([]any, []byte), error) { var values []any var bytes []byte port, done, err := PipePort( func(ch <-chan any) { for v := range ch { values = append(values, v) } }, func(r *os.File) { var err error bytes, err = io.ReadAll(r) if err != nil && err != io.EOF { logger.Println("error on reading:", err) } }, ) if err != nil { return nil, nil, err } return port, func() ([]any, []byte) { done() return values, bytes }, nil } // ValueCapturePort returns an output [*Port] whose value and byte components // are saved, with bytes saved one string value per line. It also returns a // function to call to obtain the captured output. func ValueCapturePort() (*Port, func() []any, error) { vs := []any{} var m sync.Mutex port, done, err := PipePort( func(ch <-chan any) { 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() []any { done() return vs }, nil } // StringCapturePort is like [ValueCapturePort], but converts 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 any) { 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 any, filePortChanSize) relayDone := make(chan struct{}) go func() { for v := range ch { f.WriteString(valuePrefix) f.WriteString(vals.ReprPlain(v)) 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 any) error } type valueOutput struct { data chan<- any sendStop <-chan struct{} sendError *error } func (vo valueOutput) Put(v any) 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.21.0/pkg/eval/port_helper_test.go000066400000000000000000000020201465720375400205140ustar00rootroot00000000000000package 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.21.0/pkg/eval/port_unix.go000066400000000000000000000001131465720375400171620ustar00rootroot00000000000000//go:build unix package eval import "syscall" var epipe = syscall.EPIPE elvish-0.21.0/pkg/eval/port_windows.go000066400000000000000000000005361465720375400177020ustar00rootroot00000000000000package 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.21.0/pkg/eval/process_unix.go000066400000000000000000000011741465720375400176640ustar00rootroot00000000000000//go:build unix 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.Fd()) { 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.21.0/pkg/eval/process_windows.go000066400000000000000000000005671465720375400204000ustar00rootroot00000000000000package 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.21.0/pkg/eval/purely_eval.go000066400000000000000000000033011465720375400174640ustar00rootroot00000000000000package eval import ( "strings" "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 := 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) any { 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.21.0/pkg/eval/purely_eval_test.go000066400000000000000000000031721465720375400205310ustar00rootroot00000000000000package 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.21.0/pkg/eval/pwd.go000066400000000000000000000021361465720375400157340ustar00rootroot00000000000000package 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{} // Can be mutated in tests. 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() any { 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 any) error { path, ok := v.(string) if !ok { return vars.ErrPathMustBeString } return pwd.ev.Chdir(path) } elvish-0.21.0/pkg/eval/testdata/000077500000000000000000000000001465720375400164225ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/testdata/fuzz/000077500000000000000000000000001465720375400174205ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/testdata/fuzz/FuzzCheck/000077500000000000000000000000001465720375400213145ustar00rootroot0000000000000030e5cf5b35c294c05ffc72c31859e85bd4c663bbac718db7b941824b8d74af82000066400000000000000000000000401465720375400321300ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/testdata/fuzz/FuzzCheckgo test fuzz v1 string("{}= 0") elvish-0.21.0/pkg/eval/testexport_test.go000066400000000000000000000005401465720375400204170ustar00rootroot00000000000000package eval // Pointers to variables that can be mutated for testing. var ( GetHome = &getHome Getwd = &getwd OSExit = &osExit TimeAfter = &timeAfter TimeNow = &timeNow NextEvalCount = &nextEvalCount ExceptionCauseStartMarker = &exceptionCauseStartMarker ExceptionCauseEndMarker = &exceptionCauseEndMarker ) elvish-0.21.0/pkg/eval/testutil_test.go000066400000000000000000000002011465720375400200450ustar00rootroot00000000000000package eval_test import ( "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" ) var ( It = tt.It Dedent = testutil.Dedent ) elvish-0.21.0/pkg/eval/transcripts_test.go000066400000000000000000000144671465720375400205670ustar00rootroot00000000000000package eval_test import ( "embed" "errors" "fmt" "math/rand" "strconv" "strings" "testing" "time" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/fsutil" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/transcript" ) var ( //go:embed *.elvts *.elv transcripts embed.FS transcriptNodes = must.OK1(transcript.ParseFromFS(transcripts)) transcriptCodes = extractAllCodes(transcriptNodes) ) func TestTranscripts(t *testing.T) { evaltest.TestTranscriptNodes(t, transcriptNodes, "args", func(ev *eval.Evaler, arg string) { ev.Args = vals.MakeListSlice(strings.Fields(arg)) }, "recv-bg-job-notification-in-global", func(ev *eval.Evaler) { noteCh := make(chan string, 10) ev.BgJobNotify = func(s string) { noteCh <- s } ev.ExtendGlobal(eval.BuildNs(). AddGoFn("recv-bg-job-notification", func() any { return <-noteCh })) }, "with-temp-home", func(t *testing.T) { testutil.TempHome(t) }, "reseed-afterwards", func(t *testing.T) { t.Cleanup(func() { //lint:ignore SA1019 Reseed to make other RNG-dependent tests non-deterministic rand.Seed(time.Now().UTC().UnixNano()) }) }, "check-exit-code-afterwards", func(t *testing.T, arg string) { var exitCodes []int testutil.Set(t, eval.OSExit, func(i int) { exitCodes = append(exitCodes, i) }) wantExitCode := must.OK1(strconv.Atoi(arg)) t.Cleanup(func() { if len(exitCodes) != 1 { t.Errorf("os.Exit called %d times, want once", len(exitCodes)) } else if exitCodes[0] != wantExitCode { t.Errorf("os.Exit called with %d, want %d", exitCodes[0], wantExitCode) } }) }, "check-pre-exit-hook-afterwards", func(t *testing.T, ev *eval.Evaler) { testutil.Set(t, eval.OSExit, func(int) {}) calls := 0 ev.PreExitHooks = append(ev.PreExitHooks, func() { calls++ }) t.Cleanup(func() { if calls != 1 { t.Errorf("pre-exit hook called %v times, want 1", calls) } }) }, "add-bad-var", func(ev *eval.Evaler, arg string) { name, allowedSetsString, _ := strings.Cut(arg, " ") allowedSets := must.OK1(strconv.Atoi(allowedSetsString)) ev.ExtendGlobal(eval.BuildNs().AddVar(name, &badVar{allowedSets})) }, "tmp-lib-dir", func(t *testing.T, ev *eval.Evaler) { libdir := testutil.TempDir(t) ev.LibDirs = []string{libdir} ev.ExtendGlobal(eval.BuildNs(). AddVar("lib", vars.NewReadOnly(libdir))) }, "two-tmp-lib-dirs", func(t *testing.T, ev *eval.Evaler) { libdir1 := testutil.TempDir(t) libdir2 := testutil.TempDir(t) ev.LibDirs = []string{libdir1, libdir2} ev.ExtendGlobal(eval.BuildNs(). AddVar("lib1", vars.NewReadOnly(libdir1)). AddVar("lib2", vars.NewReadOnly(libdir2))) }, "add-var-in-builtin", func(ev *eval.Evaler) { addVar := func(name string, val any) { ev.ExtendGlobal(eval.BuildNs().AddVar(name, vars.FromInit(val))) } ev.ExtendBuiltin(eval.BuildNs().AddGoFn("add-var", addVar)) }, "test-time-scale-in-global", func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs(). AddVar("test-time-scale", vars.NewReadOnly(testutil.TestTimeScale()))) }, "mock-get-home-error", func(t *testing.T, msg string) { err := errors.New(msg) testutil.Set(t, eval.GetHome, func(name string) (string, error) { return "", err }) }, "force-eval-source-count", func(t *testing.T, arg string) { c := must.OK1(strconv.Atoi(arg)) testutil.Set(t, eval.NextEvalCount, func() int { return c }) }, "mock-time-after", func(t *testing.T) { testutil.Set(t, eval.TimeAfter, func(fm *eval.Frame, d time.Duration) <-chan time.Time { fmt.Fprintf(fm.ByteOutput(), "slept for %s\n", d) return time.After(0) }) }, "mock-benchmark-run-durations", func(t *testing.T, arg string) { // The benchmark command calls time.Now once before a run and once // after a run. var ticks []int64 for i, field := range strings.Fields(arg) { d := must.OK1(strconv.ParseInt(field, 0, 64)) if i == 0 { ticks = append(ticks, 0, d) } else { last := ticks[len(ticks)-1] ticks = append(ticks, last, last+d) } } testutil.Set(t, eval.TimeNow, func() time.Time { if len(ticks) == 0 { panic("mock TimeNow called more than len(ticks)") } v := ticks[0] ticks = ticks[1:] return time.Unix(v, 0) }) }, "inject-time-after-with-sigint-or-skip", injectTimeAfterWithSIGINTOrSkip, "mock-getwd-error", func(t *testing.T, msg string) { err := errors.New(msg) testutil.Set(t, eval.Getwd, func() (string, error) { return "", err }) }, "mock-no-other-home", func(t *testing.T) { testutil.Set(t, eval.GetHome, func(name string) (string, error) { switch name { case "": return fsutil.GetHome("") default: return "", fmt.Errorf("don't know home of %v", name) } }) }, "mock-one-other-home", func(t *testing.T, ev *eval.Evaler) { otherHome := testutil.TempDir(t) ev.ExtendGlobal(eval.BuildNs().AddVar("other-home", vars.NewReadOnly(otherHome))) testutil.Set(t, eval.GetHome, func(name string) (string, error) { switch name { case "": return fsutil.GetHome("") case "other": return otherHome, nil default: return "", fmt.Errorf("don't know home of %v", name) } }) }, "go-fns-mod-in-global", func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddNs("go-fns", goFnsMod)) }, "call-hook-in-global", func(ev *eval.Evaler) { callHook := func(fm *eval.Frame, name string, hook vals.List, args ...any) { evalCfg := &eval.EvalCfg{Ports: []*eval.Port{fm.Port(0), fm.Port(1), fm.Port(2)}} eval.CallHook(fm.Evaler, evalCfg, name, hook, args...) } ev.ExtendGlobal(eval.BuildNs().AddGoFn("call-hook", callHook)) }, ) } var errBadVar = errors.New("bad var") type badVar struct{ allowedSets int } func (v *badVar) Get() any { return nil } func (v *badVar) Set(any) error { if v.allowedSets == 0 { return errBadVar } v.allowedSets-- return nil } func extractAllCodes(nodes []*transcript.Node) []string { var codes []string for _, node := range nodes { var codeBuf strings.Builder for i, interaction := range node.Interactions { if i > 0 { codeBuf.WriteByte('\n') } codeBuf.WriteString(interaction.Code) } codes = append(codes, codeBuf.String()) codes = append(codes, extractAllCodes(node.Children)...) } return codes } elvish-0.21.0/pkg/eval/transcripts_unix_test.go000066400000000000000000000005561465720375400216240ustar00rootroot00000000000000//go:build unix package eval_test import ( "os" "testing" "time" "golang.org/x/sys/unix" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/testutil" ) func injectTimeAfterWithSIGINTOrSkip(t *testing.T) { testutil.Set(t, eval.TimeAfter, func(_ *eval.Frame, d time.Duration) <-chan time.Time { go unix.Kill(os.Getpid(), unix.SIGINT) return time.After(d) }) } elvish-0.21.0/pkg/eval/transcripts_windows_test.go000066400000000000000000000001451465720375400223250ustar00rootroot00000000000000package eval_test import "testing" func injectTimeAfterWithSIGINTOrSkip(t *testing.T) { t.Skip() } elvish-0.21.0/pkg/eval/vals/000077500000000000000000000000001465720375400155565ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/vals/aliased_types.go000066400000000000000000000021171465720375400207340ustar00rootroot00000000000000package 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 ...any) vector.Vector { return MakeListSlice(vs) } // MakeListSlice creates a new List from a slice. func MakeListSlice[T any](vs []T) vector.Vector { vec := vector.Empty for _, v := range vs { vec = vec.Conj(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 ...any) 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.21.0/pkg/eval/vals/aliased_types_test.go000066400000000000000000000005001465720375400217650ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestMakeMap_PanicsWithOddNumberOfArguments(t *testing.T) { tt.Test(t, testutil.Recover, //lint:ignore SA5012 testing panic Args(func() { MakeMap("foo") }).Rets("odd number of arguments to MakeMap"), ) } elvish-0.21.0/pkg/eval/vals/assoc.go000066400000000000000000000030261465720375400172160ustar00rootroot00000000000000package 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 any) (any, 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 any) (any, 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 StructMap: return promoteToMap(a).Assoc(k, v), nil case Assocer: return a.Assoc(k, v) } return nil, errAssocUnsupported } func assocString(s string, k, v any) (any, 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 any) (any, 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.21.0/pkg/eval/vals/assoc_test.go000066400000000000000000000031031465720375400202510ustar00rootroot00000000000000package 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 any) (any, error) { return "custom result", errCustomAssoc } func TestAssoc(t *testing.T) { tt.Test(t, Assoc, // String 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), // List 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), // Map 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), // Struct map Args(testStructMap{"ls", 1.0}, "score-plus-ten", "x").Rets( eq(MakeMap("name", "ls", "score", 1.0, "score-plus-ten", "x")), nil), Args(customAssocer{}, "x", "y").Rets("custom result", errCustomAssoc), Args(struct{}{}, "x", "y").Rets(nil, errAssocUnsupported), ) } elvish-0.21.0/pkg/eval/vals/bool.go000066400000000000000000000007131465720375400170410ustar00rootroot00000000000000package 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 any) bool { switch v := v.(type) { case nil: return false case bool: return v case Booler: return v.Bool() } return true } elvish-0.21.0/pkg/eval/vals/bool_test.go000066400000000000000000000006531465720375400201030ustar00rootroot00000000000000package 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) { tt.Test(t, Bool, 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.21.0/pkg/eval/vals/cmp.go000066400000000000000000000073731465720375400166760ustar00rootroot00000000000000package vals import ( "math" "math/big" "unsafe" ) // Ordering relationship between two Elvish values. type Ordering uint8 // Possible Ordering values. const ( CmpLess Ordering = iota CmpEqual CmpMore CmpUncomparable ) // Cmp compares two Elvish values and returns the ordering relationship between // them. Cmp(a, b) returns CmpEqual iff Equal(a, b) is true or both a and b are // NaNs. func Cmp(a, b any) Ordering { return cmpInner(a, b, Cmp) } func cmpInner(a, b any, recurse func(a, b any) Ordering) Ordering { // Keep the branches in the same order as [Equal]. switch a := a.(type) { case nil: if b == nil { return CmpEqual } case bool: if b, ok := b.(bool); ok { switch { case a == b: return CmpEqual //lint:ignore S1002 using booleans as values, not conditions case a == false: // b == true is implicit return CmpLess default: // a == true && b == false return CmpMore } } case int, *big.Int, *big.Rat, float64: switch b.(type) { case int, *big.Int, *big.Rat, float64: a, b := UnifyNums2(a, b, 0) switch a := a.(type) { case int: return compareBuiltin(a, b.(int)) case *big.Int: return compareBuiltin(a.Cmp(b.(*big.Int)), 0) case *big.Rat: return compareBuiltin(a.Cmp(b.(*big.Rat)), 0) case float64: return compareFloat(a, b.(float64)) default: panic("unreachable") } } case string: if b, ok := b.(string); ok { return compareBuiltin(a, b) } case List: if b, ok := b.(List); ok { aIt := a.Iterator() bIt := b.Iterator() for aIt.HasElem() && bIt.HasElem() { o := recurse(aIt.Elem(), bIt.Elem()) if o != CmpEqual { return o } aIt.Next() bIt.Next() } switch { case a.Len() == b.Len(): return CmpEqual case a.Len() < b.Len(): return CmpLess default: // a.Len() > b.Len() return CmpMore } } default: if Equal(a, b) { return CmpEqual } } return CmpUncomparable } func compareBuiltin[T interface{ int | uintptr | string }](a, b T) Ordering { if a < b { return CmpLess } else if a > b { return CmpMore } return CmpEqual } 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 CmpEqual } return CmpLess case math.IsNaN(b): return CmpMore case a < b: return CmpLess case a > b: return CmpMore default: // a == b return CmpEqual } } // CmpTotal is similar to [Cmp], but uses an artificial total ordering to avoid // returning [CmpUncomparable]: // // - If a and b have different types, it compares their types instead. The // ordering of types is guaranteed to be consistent during one Elvish // session, but is otherwise undefined. // // - If a and b have the same type but are considered uncomparable by [Cmp], // it returns [CmpEqual] instead of [CmpUncomparable]. // // All the underlying Go types of Elvish's number type are considered the same // type. // // This function is mainly useful for sorting Elvish values that are not // considered comparable by [Cmp]. Using this function as a comparator groups // values by their types and sorts types that are comparable. func CmpTotal(a, b any) Ordering { if o := compareBuiltin(typeOf(a), typeOf(b)); o != CmpEqual { return o } if o := cmpInner(a, b, CmpTotal); o != CmpUncomparable { return o } return CmpEqual } var typeOfInt, typeOfMap uintptr func typeOf(x any) uintptr { switch x.(type) { case *big.Int, *big.Rat, float64: return typeOfInt case StructMap: return typeOfMap } // The first word of an empty interface is a pointer to the type descriptor. return *(*uintptr)(unsafe.Pointer(&x)) } func init() { typeOfInt = typeOf(0) typeOfMap = typeOf(EmptyMap) } elvish-0.21.0/pkg/eval/vals/cmp_test.go000066400000000000000000000013171465720375400177250ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) func TestCmp(t *testing.T) { // Cmp is tested by tests of the Elvish compare command. } func TestCmpTotal_StructMap(t *testing.T) { // CmpTotal should pretend that structmaps are maps too. Since maps don't // have an internal ordering, comparing a structmap to another structmap or // to a map should always return CmpEqual, like comparing two maps. // // This is not covered by tests of the Elvish compare command because Elvish // code are not supposed to know which values are actually structmaps. x := testStructMap{} y := testStructMap2{} z := EmptyMap tt.Test(t, CmpTotal, tt.Args(x, y).Rets(CmpEqual), tt.Args(x, z).Rets(CmpEqual), ) } elvish-0.21.0/pkg/eval/vals/concat.go000066400000000000000000000036451465720375400173640ustar00rootroot00000000000000package 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 any) (any, error) } // RConcatter wraps the RConcat method. See Concat for how it is used. type RConcatter interface { RConcat(v any) (any, 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 any) (any, 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 any) (any, 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.21.0/pkg/eval/vals/concat_test.go000066400000000000000000000032561465720375400204210ustar00rootroot00000000000000package 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 any) (any, 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 any) (any, error) { return "rconcatter", nil } func TestConcat(t *testing.T) { tt.Test(t, Concat, 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.21.0/pkg/eval/vals/conversion.go000066400000000000000000000201161465720375400202720ustar00rootroot00000000000000package vals import ( "errors" "fmt" "math/big" "reflect" "strconv" "sync" "unicode/utf8" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/strutil" ) // Conversion between "Go values" (those expected by native Go functions) and // "Elvish values" (those participating in the Elvish runtime). // // Among the conversion functions, [ScanToGo] and [FromGo] implement the // implicit conversion used when calling native Go functions from Elvish. The // API is asymmetric; this has to do with two characteristics of Elvish's type // system: // // - Elvish doesn't have a dedicated rune type and uses strings to represent // them. // // - Elvish permits using strings that look like numbers in place of numbers. // // As a result, while FromGo can always convert a "Go value" to an "Elvish // value" unambiguously, ScanToGo can't do that in the opposite direction. // For example, "1" may be converted into "1", '1' or 1, depending on what // the destination type is, and the process may fail. Thus ScanToGo takes the // pointer to the destination as an argument, and returns an error. // // The rest of the conversion functions are exported for use in more // sophisticated binding code, and need to explicitly invoked. // WrongType is returned by ScanToGo if the source value doesn't have a // compatible type. type WrongType struct { wantKind string gotKind string } // Error implements the error interface. 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, and stores it in the destination of ptr, // which must be a pointer. // // If ptr has type *int, *float64, *Num or *rune, it performs a suitable // conversion, and returns an error if the conversion fails. In other cases, // this function just tries to perform "*ptr = src" via reflection and returns // an error if the assignment can't be done. func ScanToGo(src any, ptr any) 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() // Allow using any(nil) as T(nil) for any T whose zero value is spelt // nil. if src == nil { switch dstType.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: ValueOf(ptr).Elem().SetZero() return nil } } 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 } } func elvToInt(arg any) (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", ReprPlain(arg)} default: return 0, errMustBeInteger } } func elvToNum(arg any) (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", ReprPlain(arg)} } return n, nil default: return 0, errMustBeNumber } } func elvToRune(arg any) (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 } // ScanListToGo converts a List to a slice, using ScanToGo to convert each // element. func ScanListToGo(src List, ptr any) error { n := src.Len() values := reflect.MakeSlice(reflect.TypeOf(ptr).Elem(), n, n) i := 0 for it := src.Iterator(); it.HasElem(); it.Next() { err := ScanToGo(it.Elem(), values.Index(i).Addr().Interface()) if err != nil { return err } i++ } reflect.ValueOf(ptr).Elem().Set(values) return nil } // Optional wraps the last pointer passed to ScanListElementsToGo, to indicate // that it is optional. func Optional(ptr any) any { return optional{ptr} } type optional struct{ ptr any } // ScanListElementsToGo unpacks elements from a list, storing the each element // in the given pointers with ScanToGo. // // The last pointer may be wrapped with Optional to indicate that it is // optional. func ScanListElementsToGo(src List, ptrs ...any) error { if o, ok := ptrs[len(ptrs)-1].(optional); ok { switch src.Len() { case len(ptrs) - 1: ptrs = ptrs[:len(ptrs)-1] case len(ptrs): ptrs[len(ptrs)-1] = o.ptr default: return errs.ArityMismatch{What: "list elements", ValidLow: len(ptrs) - 1, ValidHigh: len(ptrs), Actual: src.Len()} } } else if src.Len() != len(ptrs) { return errs.ArityMismatch{What: "list elements", ValidLow: len(ptrs), ValidHigh: len(ptrs), Actual: src.Len()} } i := 0 for it := src.Iterator(); it.HasElem(); it.Next() { err := ScanToGo(it.Elem(), ptrs[i]) if err != nil { return err } i++ } return nil } // ScanMapToGo scans map elements into ptr, which must be a pointer to a struct. // Struct field names are converted to map keys with CamelToDashed. // // The map may contains keys that don't correspond to struct fields, and it // doesn't have to contain all keys that correspond to struct fields. func ScanMapToGo(src Map, ptr any) error { // Iterate over the struct keys instead of the map: since extra keys are // allowed, the map may be very big, while the size of the struct is bound. keys, _ := StructFieldsInfo(reflect.TypeOf(ptr).Elem()) structValue := reflect.ValueOf(ptr).Elem() for i, key := range keys { if key == "" { continue } val, ok := src.Index(key) if !ok { continue } err := ScanToGo(val, structValue.Field(i).Addr().Interface()) if err != nil { return err } } return nil } // StructFieldsInfo takes a type for a struct, and returns a slice for each // field name, converted with CamelToDashed, and a reverse index. Unexported // fields result in an empty string in the slice, and is omitted from the // reverse index. func StructFieldsInfo(t reflect.Type) ([]string, map[string]int) { if info, ok := structFieldsInfoCache.Load(t); ok { info := info.(structFieldsInfo) return info.keys, info.keyIdx } info := makeStructFieldsInfo(t) structFieldsInfoCache.Store(t, info) return info.keys, info.keyIdx } var structFieldsInfoCache sync.Map type structFieldsInfo struct { keys []string keyIdx map[string]int } func makeStructFieldsInfo(t reflect.Type) structFieldsInfo { keys := make([]string, t.NumField()) keyIdx := make(map[string]int) for i := 0; i < t.NumField(); i++ { field := t.Field(i) if field.PkgPath != "" { continue } key := strutil.CamelToDashed(field.Name) keyIdx[key] = i keys[i] = key } return structFieldsInfo{keys, keyIdx} } // FromGo converts a Go value to an Elvish value. // // Exact numbers are normalized to the smallest types that can hold them, and // runes are converted to strings. Values of other types are returned unchanged. func FromGo(a any) any { switch a := a.(type) { case *big.Int: return NormalizeBigInt(a) case *big.Rat: return NormalizeBigRat(a) case rune: return string(a) default: return a } } elvish-0.21.0/pkg/eval/vals/conversion_test.go000066400000000000000000000147201465720375400213350ustar00rootroot00000000000000package vals import ( "math/big" "reflect" "testing" "src.elv.sh/pkg/eval/errs" "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 any, dstInit any) (any, error) { ptr := reflect.New(TypeOf(dstInit)) err := ScanToGo(src, ptr.Interface()) return ptr.Elem().Interface(), err } tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), // 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(tt.Any, errMustBeInteger), Args("x", 0).Rets(tt.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(tt.Any, errMustBeNumber), Args("x", 0.0).Rets(tt.Any, cannotParseAs{"number", "x"}), // rune Args("x", ' ').Rets('x'), Args(someType{}, ' ').Rets(tt.Any, errMustBeString), Args("\xc3\x28", ' ').Rets(tt.Any, errMustBeValidUTF8), // Invalid UTF8 Args("ab", ' ').Rets(tt.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(tt.Any, WrongType{"!!vals.someType", "string"}), ) } func TestScanToGo_NumDst(t *testing.T) { scanToGo := func(src any) (Num, error) { var n Num err := ScanToGo(src, &n) return n, err } tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), // 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(tt.Any, cannotParseAs{"number", "bad"}), Args(EmptyList).Rets(tt.Any, errMustBeNumber), ) } func TestScanToGo_InterfaceDst(t *testing.T) { scanToGo := func(src any) (any, error) { var l List err := ScanToGo(src, &l) return l, err } tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), Args(EmptyList).Rets(EmptyList), Args("foo").Rets(tt.Any, WrongType{"!!vector.Vector", "string"}), ) } func TestScanToGo_CallableDstAdmitsNil(t *testing.T) { type mockCallable interface { Call() } scanToGo := func(src any) (any, error) { var c mockCallable err := ScanToGo(src, &c) return c, err } tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), Args(nil).Rets(mockCallable(nil)), ) } func TestScanToGo_ErrorsWithNonPointerDst(t *testing.T) { err := ScanToGo("", 1) if err == nil { t.Errorf("did not return error") } } func TestScanListToGo(t *testing.T) { // A wrapper around ScanListToGo, to make it easier to test. scanListToGo := func(src List, dstInit any) (any, error) { ptr := reflect.New(TypeOf(dstInit)) ptr.Elem().Set(reflect.ValueOf(dstInit)) err := ScanListToGo(src, ptr.Interface()) return ptr.Elem().Interface(), err } tt.Test(t, tt.Fn(scanListToGo).Named("scanListToGo"), Args(MakeList("1", "2"), []int{}).Rets([]int{1, 2}), Args(MakeList("1", "2"), []string{}).Rets([]string{"1", "2"}), Args(MakeList("1", "a"), []int{}).Rets([]int{}, cannotParseAs{"integer", "a"}), ) } func TestScanListElementsToGo(t *testing.T) { // A wrapper around ScanListElementsToGo, to make it easier to test. scanListElementsToGo := func(src List, inits ...any) ([]any, error) { ptrs := make([]any, len(inits)) for i, init := range inits { if o, ok := init.(optional); ok { // Wrapping the init value with Optional translates to wrapping // the pointer with Optional. ptrs[i] = Optional(reflect.New(TypeOf(o.ptr)).Interface()) } else { ptrs[i] = reflect.New(TypeOf(init)).Interface() } } err := ScanListElementsToGo(src, ptrs...) vals := make([]any, len(ptrs)) for i, ptr := range ptrs { if o, ok := ptr.(optional); ok { vals[i] = reflect.ValueOf(o.ptr).Elem().Interface() } else { vals[i] = reflect.ValueOf(ptr).Elem().Interface() } } return vals, err } tt.Test(t, tt.Fn(scanListElementsToGo).Named("scanListElementsToGo"), Args(MakeList("1", "2"), 0, 0).Rets([]any{1, 2}), Args(MakeList("1", "2"), "", "").Rets([]any{"1", "2"}), Args(MakeList("1", "2"), 0, Optional(0)).Rets([]any{1, 2}), Args(MakeList("1"), 0, Optional(0)).Rets([]any{1, 0}), Args(MakeList("a"), 0).Rets([]any{0}, cannotParseAs{"integer", "a"}), Args(MakeList("1"), 0, 0).Rets([]any{0, 0}, errs.ArityMismatch{What: "list elements", ValidLow: 2, ValidHigh: 2, Actual: 1}), Args(MakeList("1"), 0, 0, Optional(0)).Rets([]any{0, 0, 0}, errs.ArityMismatch{What: "list elements", ValidLow: 2, ValidHigh: 3, Actual: 1}), ) } type aStruct struct { Foo int bar any } // Equal is required by cmp.Diff, since aStruct contains unexported fields. func (a aStruct) Equal(b aStruct) bool { return a == b } func TestScanMapToGo(t *testing.T) { // A wrapper around ScanMapToGo, to make it easier to test. scanMapToGo := func(src Map, dstInit any) (any, error) { ptr := reflect.New(TypeOf(dstInit)) ptr.Elem().Set(reflect.ValueOf(dstInit)) err := ScanMapToGo(src, ptr.Interface()) return ptr.Elem().Interface(), err } tt.Test(t, tt.Fn(scanMapToGo).Named("scanMapToGo"), Args(MakeMap("foo", "1"), aStruct{}).Rets(aStruct{Foo: 1}), // More fields is OK Args(MakeMap("foo", "1", "bar", "x"), aStruct{}).Rets(aStruct{Foo: 1}), // Fewer fields is OK Args(MakeMap(), aStruct{}).Rets(aStruct{}), // Unexported fields are ignored Args(MakeMap("bar", 20), aStruct{bar: 10}).Rets(aStruct{bar: 10}), // Conversion error Args(MakeMap("foo", "a"), aStruct{}). Rets(aStruct{}, cannotParseAs{"integer", "a"}), ) } func TestFromGo(t *testing.T) { tt.Test(t, FromGo, // 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.21.0/pkg/eval/vals/dissoc.go000066400000000000000000000012271465720375400173730ustar00rootroot00000000000000package 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 any) any } // 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 any) any { switch a := a.(type) { case Map: return a.Dissoc(k) case StructMap: return promoteToMap(a).Dissoc(k) case Dissocer: return a.Dissoc(k) default: return nil } } elvish-0.21.0/pkg/eval/vals/dissoc_test.go000066400000000000000000000007031465720375400204300ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) type dissocer struct{} func (dissocer) Dissoc(any) any { return "custom ret" } func TestDissoc(t *testing.T) { tt.Test(t, Dissoc, Args(MakeMap("k1", "v1", "k2", "v2"), "k1"). Rets(eq(MakeMap("k2", "v2"))), Args(testStructMap{"ls", 1.0}, "score-plus-ten"). Rets(eq(MakeMap("name", "ls", "score", 1.0))), Args(dissocer{}, "x").Rets("custom ret"), Args("", "x").Rets(nil), ) } elvish-0.21.0/pkg/eval/vals/doc.go000066400000000000000000000001561465720375400166540ustar00rootroot00000000000000// Package vals contains basic facilities for manipulating values used in the // Elvish runtime. package vals elvish-0.21.0/pkg/eval/vals/equal.go000066400000000000000000000042521465720375400172170ustar00rootroot00000000000000package vals import ( "math/big" "reflect" "src.elv.sh/pkg/persistent/hashmap" ) // 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 any) 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 any) bool { switch x := x.(type) { case nil: return x == y case bool: return x == y case int: 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 float64: return x == y case string: return x == y case List: if yy, ok := y.(List); ok { return equalList(x, yy) } return false // Types above are also handled in [Cmp]; keep the branches in the same // order. case Equaler: return x.Equal(y) case File: if yy, ok := y.(File); ok { return x.Fd() == yy.Fd() } return false case Map: switch y := y.(type) { case Map: return equalMap(x, y, Map.Iterator, Map.Index) case StructMap: return equalMap(x, y, Map.Iterator, indexStructMap) } return false case StructMap: switch y := y.(type) { case Map: return equalMap(x, y, iterateStructMap, Map.Index) case StructMap: return equalMap(x, y, iterateStructMap, indexStructMap) } 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 any, I hashmap.Iterator](x X, y Y, xit func(X) I, yidx func(Y, any) (any, bool)) bool { if Len(x) != Len(y) { return false } for it := xit(x); it.HasElem(); it.Next() { k, vx := it.Elem() vy, ok := yidx(y, k) if !ok || !Equal(vx, vy) { return false } } return true } elvish-0.21.0/pkg/eval/vals/equal_test.go000066400000000000000000000032121465720375400202510ustar00rootroot00000000000000package vals import ( "math/big" "os" "testing" "src.elv.sh/pkg/tt" ) type customEqualer struct{ ret bool } func (c customEqualer) Equal(any) bool { return c.ret } type customStruct struct{ a, b string } func TestEqual(t *testing.T) { tt.Test(t, Equal, 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.21.0/pkg/eval/vals/errors_test.go000066400000000000000000000003711465720375400204610ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) func TestErrors(t *testing.T) { tt.Test(t, error.Error, Args(cannotIterate{"num"}).Rets("cannot iterate num"), Args(cannotIterateKeysOf{"num"}).Rets("cannot iterate keys of num"), ) } elvish-0.21.0/pkg/eval/vals/feed.go000066400000000000000000000003371465720375400170130ustar00rootroot00000000000000package vals // Feed calls the function with given values, breaking earlier if the function // returns false. func Feed(f func(any) bool, values ...any) { for _, value := range values { if !f(value) { break } } } elvish-0.21.0/pkg/eval/vals/feed_test.go000066400000000000000000000004641465720375400200530ustar00rootroot00000000000000package vals import ( "reflect" "testing" ) func TestFeed(t *testing.T) { var fed []any Feed(func(x any) bool { fed = append(fed, x) return x != 10 }, 1, 2, 3, 10, 11, 12, 13) wantFed := []any{1, 2, 3, 10} if !reflect.DeepEqual(fed, wantFed) { t.Errorf("Fed %v, want %v", fed, wantFed) } } elvish-0.21.0/pkg/eval/vals/has_key.go000066400000000000000000000027731465720375400175410ustar00rootroot00000000000000package 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(any) 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 any) 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 PseudoMap: return hasKeyStructMap(container.Fields(), key) default: var found bool err := IterateKeys(container, func(k any) 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 any) 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.21.0/pkg/eval/vals/has_key_test.go000066400000000000000000000024401465720375400205670ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) type hasKeyer struct{ key any } func (h hasKeyer) HasKey(k any) bool { return k == h.key } func TestHasKey(t *testing.T) { tt.Test(t, HasKey, // 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"), "0..=").Rets(true), Args(MakeList("lorem", "ipsum"), "..2").Rets(true), Args(MakeList("lorem", "ipsum"), "..=2").Rets(false), Args(MakeList("lorem", "ipsum"), "2").Rets(false), Args(MakeList("lorem", "ipsum", "dolor", "sit"), "0..4").Rets(true), Args(MakeList("lorem", "ipsum", "dolor", "sit"), "0..=4").Rets(false), Args(MakeList("lorem", "ipsum", "dolor", "sit"), "1..3").Rets(true), Args(MakeList("lorem", "ipsum", "dolor", "sit"), "1..5").Rets(false), Args(MakeList("lorem", "ipsum", "dolor", "sit"), "-2..=-1").Rets(true), // Non-container Args(1, "0").Rets(false), ) } elvish-0.21.0/pkg/eval/vals/hash.go000066400000000000000000000037261465720375400170400ustar00rootroot00000000000000package vals import ( "math" "math/big" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/persistent/hashmap" ) // 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 any) 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: return hashMap(v.Iterator()) case StructMap: return hashMap(iterateStructMap(v)) } return 0 } func hashMap(it hashmap.Iterator) uint32 { // The iteration order of maps only depends on the hash of the keys. It is // almost deterministic, with only one exception: when two keys have the // same hash, they get produced in insertion order. As a result, it is // possible for two maps that should be considered equal to produce entries // in different orders. // // So instead of using hash.DJBCombine, combine the hash result from each // key-value pair by summing, so that the order doesn't matter. // // TODO: This may not have very good hashing properties. var h uint32 for ; it.HasElem(); it.Next() { k, v := it.Elem() h += hash.DJB(Hash(k), Hash(v)) } return h } elvish-0.21.0/pkg/eval/vals/hash_test.go000066400000000000000000000030351465720375400200700ustar00rootroot00000000000000package vals import ( "math" "math/big" "os" "testing" "unsafe" "src.elv.sh/pkg/persistent/hash" "src.elv.sh/pkg/persistent/hashmap" "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 tt.Test(t, Hash, 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)), ) } func TestHash_EqualMapsWithDifferentInternal(t *testing.T) { // The internal representation of maps with the same value is not always the // same: when some keys of the map have the same hash, their values are // stored in the insertion order. // // To reliably test this case, we construct maps with a custom hashing // function. m0 := hashmap.New(Equal, func(v any) uint32 { return 0 }) m1 := m0.Assoc("k1", "v1").Assoc("k2", "v2") m2 := m0.Assoc("k2", "v2").Assoc("k1", "v1") if h1, h2 := Hash(m1), Hash(m2); h1 != h2 { t.Errorf("%v != %v", h1, h2) } } elvish-0.21.0/pkg/eval/vals/index.go000066400000000000000000000040761465720375400172230ustar00rootroot00000000000000package vals import ( "errors" "os" ) // 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 any) (v any, ok bool) } // ErrIndexer wraps the Index method. type ErrIndexer interface { // Index retrieves one value from the receiver at the specified index. Index(k any) (any, error) } var errNotIndexable = errors.New("not indexable") type noSuchKeyError struct { key any } // NoSuchKey returns an error indicating that a key is not found in a map-like // value. func NoSuchKey(k any) error { return noSuchKeyError{k} } func (err noSuchKeyError) Error() string { return "no such key: " + ReprPlain(err.key) } // 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 any) (any, error) { convertResult := func(v any, ok bool) (any, error) { if !ok { return nil, NoSuchKey(k) } return v, nil } 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: return convertResult(a.Index(k)) case List: return indexList(a, k) case StructMap: return convertResult(indexStructMap(a, k)) case PseudoMap: return convertResult(indexStructMap(a.Fields(), k)) default: return nil, errNotIndexable } } func indexFile(f *os.File, k any) (any, 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 any) (any, bool) { fieldName, ok := k.(string) if !ok || fieldName == "" { return nil, false } for it := iterateStructMap(a); it.HasElem(); it.Next() { k, v := it.elem() if k == fieldName { return FromGo(v), true } } return nil, false } elvish-0.21.0/pkg/eval/vals/index_list.go000066400000000000000000000074451465720375400202610ustar00rootroot00000000000000package vals import ( "errors" "strconv" "strings" "src.elv.sh/pkg/eval/errs" ) var ( errIndexMustBeInteger = errors.New("index must be integer") ) func indexList(l List, rawIndex any) (any, 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 any, 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 if j == -1 { // subtle corner case that is same as no high value j = n } else { 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.21.0/pkg/eval/vals/index_string.go000066400000000000000000000021471465720375400206060ustar00rootroot00000000000000package vals import ( "errors" "unicode/utf8" ) var errIndexNotAtRuneBoundary = errors.New("index not at rune boundary") func indexString(s string, index any) (string, error) { i, j, err := convertStringIndex(index, s) if err != nil { return "", err } return s[i:j], nil } func convertStringIndex(rawIndex any, 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.21.0/pkg/eval/vals/index_test.go000066400000000000000000000115771465720375400202660ustar00rootroot00000000000000package 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) { tt.Test(t, Index, // 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(tt.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), Args("abc", "..=-1").Rets("abc", nil), // String slices not at rune boundary. Args("你好", "2..").Rets(tt.Any, errIndexNotAtRuneBoundary), Args("你好", "..2").Rets(tt.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(tt.Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "-1", Actual: "0"}), Args(li4, "4").Rets(tt.Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "3", Actual: "4"}), Args(li4, "5").Rets(tt.Any, errs.OutOfRange{ What: "index", ValidLow: "0", ValidHigh: "3", Actual: "5"}), Args(li4, z).Rets(tt.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(tt.Any, errs.OutOfRange{ What: "negative index", ValidLow: "-4", ValidHigh: "-1", Actual: "-5"}), Args(li4, "-"+z).Rets(tt.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(tt.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), Args(li4, "..=-1").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(tt.Any, errIndexMustBeInteger), // TODO(xiaq): Make the error more accurate. Args(li4, "1:3:2").Rets(tt.Any, errIndexMustBeInteger), // Map indices // ============ Args(m, "foo").Rets("bar", nil), Args(m, "bad").Rets(tt.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) } tt.Test(t, Index, Args(f, "fd").Rets(int(f.Fd()), nil), Args(f, "name").Rets(f.Name(), nil), Args(f, "x").Rets(nil, NoSuchKey("x")), ) } elvish-0.21.0/pkg/eval/vals/iterate.go000066400000000000000000000031341465720375400175430ustar00rootroot00000000000000package 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 any) 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 any) 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 any, f func(any) 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 any) ([]any, error) { var vs []any if len := Len(it); len >= 0 { vs = make([]any, 0, len) } err := Iterate(it, func(v any) bool { vs = append(vs, v) return true }) return vs, err } elvish-0.21.0/pkg/eval/vals/iterate_keys.go000066400000000000000000000025611465720375400206010ustar00rootroot00000000000000package 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 any) 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 any, f func(any) 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 PseudoMap: iterateKeysStructMap(v.Fields(), f) default: return cannotIterateKeysOf{Kind(v)} } return nil } func iterateKeysStructMap(v StructMap, f func(any) bool) { for _, k := range getStructMapInfo(reflect.TypeOf(v)).fieldNames { if k == "" { continue } if !f(k) { break } } } elvish-0.21.0/pkg/eval/vals/iterate_keys_test.go000066400000000000000000000026321465720375400216370ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) func vs(xs ...any) []any { return xs } type keysIterator struct{ keys []any } func (k keysIterator) IterateKeys(f func(any) bool) { Feed(f, k.keys...) } type nonKeysIterator struct{} func TestIterateKeys(t *testing.T) { tt.Test(t, tt.Fn(collectKeys).Named("collectKeys"), Args(MakeMap("k1", "v1", "k2", "v2")).Rets(vs("k1", "k2"), nil), Args(keysIterator{vs("lorem", "ipsum")}).Rets(vs("lorem", "ipsum")), Args(nonKeysIterator{}).Rets( tt.Any, cannotIterateKeysOf{"!!vals.nonKeysIterator"}), ) } func TestIterateKeys_Map_Break(t *testing.T) { var gotKey any IterateKeys(MakeMap("k", "v", "k2", "v2"), func(k any) 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 any IterateKeys(testStructMap{}, func(k any) 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(any) bool { return true }) wantErr := cannotIterateKeysOf{"number"} if err != wantErr { t.Errorf("got error %v, want %v", err, wantErr) } } elvish-0.21.0/pkg/eval/vals/iterate_test.go000066400000000000000000000015031465720375400206000ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) // An implementation of Iterator. type iterator struct{ elements []any } func (i iterator) Iterate(f func(any) bool) { Feed(f, i.elements...) } // A non-implementation of Iterator. type nonIterator struct{} func TestCanIterate(t *testing.T) { tt.Test(t, CanIterate, 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) { tt.Test(t, Collect, 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.21.0/pkg/eval/vals/kind.go000066400000000000000000000021311465720375400170270ustar00rootroot00000000000000package 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 any) 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 File: return "file" case List: return "list" case Map, StructMap: return "map" case Kinder: return v.Kind() default: return fmt.Sprintf("!!%T", v) } } elvish-0.21.0/pkg/eval/vals/kind_test.go000066400000000000000000000010051465720375400200650ustar00rootroot00000000000000package vals import ( "math/big" "os" "testing" "src.elv.sh/pkg/tt" ) type xtype int func TestKind(t *testing.T) { tt.Test(t, Kind, 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.21.0/pkg/eval/vals/len.go000066400000000000000000000013251465720375400166640ustar00rootroot00000000000000package 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 any) int { switch v := v.(type) { case string: return len(v) case Lener: return v.Len() case StructMap: return lenStructMap(v) } return -1 } func lenStructMap(m StructMap) int { return getStructMapInfo(reflect.TypeOf(m)).filledFields } elvish-0.21.0/pkg/eval/vals/len_test.go000066400000000000000000000002351465720375400177220ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/tt" ) func TestLen(t *testing.T) { tt.Test(t, Len, Args("foobar").Rets(6), Args(10).Rets(-1), ) } elvish-0.21.0/pkg/eval/vals/num.go000066400000000000000000000160071465720375400167100ustar00rootroot00000000000000package 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 any // 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 any // 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 (use 0 if no minimal // NumType is needed). 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 } // Int64ToNum converts an int64 to a Num with a suitable underlying // representation. func Int64ToNum(i64 int64) Num { if i := int(i64); int64(i) == i64 { return i } return big.NewInt(i64) } // Uint64ToNum converts a uint64 to a Num with a suitable underlying // representation. func Uint64ToNum(u64 uint64) Num { if i := int(u64); i >= 0 && uint64(i) == u64 { return i } return new(big.Int).SetUint64(u64) } elvish-0.21.0/pkg/eval/vals/num_test.go000066400000000000000000000063371465720375400177540ustar00rootroot00000000000000package 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) { tt.Test(t, ParseNum, 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) { tt.Test(t, UnifyNums, 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) { tt.Test(t, UnifyNums2, 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) { tt.Test(t, testutil.Recover, 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 TestInt64ToNum(t *testing.T) { n := Int64ToNum(1) if _, isInt := n.(int); !isInt { t.Errorf("got %T, want int", n) } if math.MaxInt != math.MaxInt64 { n = Int64ToNum(math.MaxInt64) if _, isBigInt := n.(*big.Int); !isBigInt { t.Errorf("got %T, want *big.Int", n) } } } func TestUint64ToNum(t *testing.T) { n := Uint64ToNum(1) if _, isInt := n.(int); !isInt { t.Errorf("got %T, want int", n) } n = Uint64ToNum(math.MaxUint64) if _, isBigInt := n.(*big.Int); !isBigInt { t.Errorf("got %T, want *big.Int", n) } } 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.21.0/pkg/eval/vals/pipe.go000066400000000000000000000002441465720375400170420ustar00rootroot00000000000000package vals import ( "os" ) // Pipe wraps a pair of [*os.File] that are the two ends of a pipe. type Pipe struct{ R, W *os.File } func (Pipe) IsStructMap() {} elvish-0.21.0/pkg/eval/vals/reflect_wrappers.go000066400000000000000000000012711465720375400214550ustar00rootroot00000000000000package vals import "reflect" var ( dummy any 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 any) 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 any) reflect.Type { if i == nil { return emptyInterfaceType } return reflect.TypeOf(i) } elvish-0.21.0/pkg/eval/vals/repr.go000066400000000000000000000057041465720375400170630ustar00rootroot00000000000000package vals import ( "fmt" "math" "math/big" "sort" "strconv" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/persistent/hashmap" ) // 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 } // ReprPlain is like Repr, but without pretty-printing. func ReprPlain(v any) string { return Repr(v, math.MinInt) } // Repr returns the representation for a value, a string that is preferably // (but not necessarily) an Elvish expression that evaluates to the argument. // The representation is pretty-printed, using indent as the initial level of // indentation. 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 any, 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 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: return reprMap(v.Iterator(), v.Len(), indent) case StructMap: return reprMap(iterateStructMap(v), lenStructMap(v), indent) case Reprer: return v.Repr(indent) case PseudoMap: m := v.Fields() s := reprMap(iterateStructMap(m), lenStructMap(m), indent) // Add a tag immediately after [. return "[^" + Kind(v) + " " + s[1:] default: return fmt.Sprintf("", v) } } func reprMap(it hashmap.Iterator, n, indent int) string { builder := NewMapReprBuilder(indent) // Collect all the key-value pairs. pairs := make([][2]any, 0, n) for ; it.HasElem(); it.Next() { k, v := it.Elem() pairs = append(pairs, [2]any{k, v}) } // Sort the pairs. See the godoc of CmpTotal for the sorting algorithm. sort.Slice(pairs, func(i, j int) bool { return CmpTotal(pairs[i][0], pairs[j][0]) == CmpLess }) // Print the pairs. for _, pair := range pairs { k, v := pair[0], pair[1] builder.WritePair(Repr(k, indent+1), indent+2, Repr(v, indent+2)) } return builder.String() } elvish-0.21.0/pkg/eval/vals/repr_helpers.go000066400000000000000000000034651465720375400206070ustar00rootroot00000000000000package 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.21.0/pkg/eval/vals/repr_test.go000066400000000000000000000031641465720375400201200ustar00rootroot00000000000000package 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 TestReprPlain(t *testing.T) { tt.Test(t, ReprPlain, 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]"), // Keys of the same type are sorted. Args(MakeMap("b", "second", "a", "first", "c", "third")). Rets("[&a=first &b=second &c=third]"), Args(MakeMap(2, "second", 1, "first", 3, "third")). Rets("[&(num 1)=first &(num 2)=second &(num 3)=third]"), // Keys of mixed types tested in a different test. Args(reprer{}).Rets(""), Args(nonReprer{}).Rets(""), ) } func TestReprPlain_MapWithKeysOfMixedTypes(t *testing.T) { m := MakeMap( "b", "second", "a", "first", "c", "third", 2, "second", 1, "first", 3, "third") strPart := "&a=first &b=second &c=third" numPart := "&(num 1)=first &(num 2)=second &(num 3)=third" want1 := "[" + strPart + " " + numPart + "]" want2 := "[" + numPart + " " + strPart + "]" got := ReprPlain(m) if got != want1 && got != want2 { t.Errorf("got %q, want %q or %q", got, want1, want2) } } elvish-0.21.0/pkg/eval/vals/string.go000066400000000000000000000027261465720375400174220ustar00rootroot00000000000000package 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). func ToString(v any) 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 ReprPlain(v) } } 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.21.0/pkg/eval/vals/string_test.go000066400000000000000000000013731465720375400204560ustar00rootroot00000000000000package vals import ( "bytes" "testing" "src.elv.sh/pkg/tt" ) func TestToString(t *testing.T) { tt.Test(t, ToString, // 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.21.0/pkg/eval/vals/struct_map.go000066400000000000000000000071411465720375400202710ustar00rootroot00000000000000package vals import ( "reflect" "sync" "src.elv.sh/pkg/strutil" ) // StructMap may be implemented by a struct to make it accessible to Elvish code // as a 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. // // Struct maps are indistinguishable from normal maps for Elvish code. The // operations Kind, Repr, Hash, Equal, Len, Index, HasKey and IterateKeys handle // struct maps consistently with maps; the Assoc and Dissoc operations convert // struct maps to maps. // // Example: // // type someStruct struct { // // Provides the "foo-bar" field // FooBar int // lorem string // } // // // Marks someStruct as a struct map // func (someStruct) IsStructMap() { } // // // Provides the "ipsum" field // func (s SomeStruct) Ipsum() string { return s.lorem } // // // Not a getter method; doesn't provide any field // func (s SomeStruct) OtherMethod(int) { } type StructMap interface{ IsStructMap() } func promoteToMap(v StructMap) Map { m := EmptyMap for it := iterateStructMap(v); it.HasElem(); it.Next() { m = m.Assoc(it.Elem()) } return m } // PseudoMap may be implemented by a type to support map-like introspection. The // Repr, Index, HasKey and IterateKeys operations handle pseudo maps. type PseudoMap 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 { m reflect.Value info structMapInfo index int } func iterateStructMap(m StructMap) *structMapIterator { it := &structMapIterator{reflect.ValueOf(m), getStructMapInfo(reflect.TypeOf(m)), 0} it.fixIndex() return it } func (it *structMapIterator) fixIndex() { fieldNames := it.info.fieldNames for it.index < len(fieldNames) && fieldNames[it.index] == "" { it.index++ } } func (it *structMapIterator) Elem() (any, any) { return it.elem() } func (it *structMapIterator) elem() (string, any) { name := it.info.fieldNames[it.index] if it.index < it.info.plainFields { return name, it.m.Field(it.index).Interface() } method := it.m.Method(it.index - it.info.plainFields) return name, method.Call(nil)[0].Interface() } func (it *structMapIterator) HasElem() bool { return it.index < len(it.info.fieldNames) } func (it *structMapIterator) Next() { it.index++ it.fixIndex() } elvish-0.21.0/pkg/eval/vals/struct_map_test.go000066400000000000000000000042151465720375400213270ustar00rootroot00000000000000package vals import ( "testing" "src.elv.sh/pkg/persistent/hash" ) type testStructMap struct { Name string Score float64 } func (testStructMap) IsStructMap() {} func (m testStructMap) ScorePlusTen() float64 { return m.Score + 10 } // Equivalent to testStructMap for Elvish. type testStructMap2 struct { Name string Score float64 ScorePlusTen float64 } func (testStructMap2) IsStructMap() {} func TestStructMap(t *testing.T) { TestValue(t, testStructMap{"ls", 1.0}). Kind("map"). Bool(true). Hash( hash.DJB(Hash("name"), Hash("ls"))+ hash.DJB(Hash("score"), Hash(1.0))+ hash.DJB(Hash("score-plus-ten"), Hash(11.0))). Repr(`[&name=ls &score=(num 1.0) &score-plus-ten=(num 11.0)]`). Len(3). Equal( // Struct maps behave like maps, so they are equal to normal maps // and other struct maps with the same entries. MakeMap("name", "ls", "score", 1.0, "score-plus-ten", 11.0), testStructMap{"ls", 1.0}, testStructMap2{"ls", 1.0, 11.0}). NotEqual("a", MakeMap(), testStructMap{"ls", 2.0}, testStructMap{"l", 1.0}). HasKey("name", "score", "score-plus-ten"). HasNoKey("bad", 1.0). IndexError("bad", NoSuchKey("bad")). IndexError(1.0, NoSuchKey(1.0)). AllKeys("name", "score", "score-plus-ten"). Index("name", "ls"). Index("score", 1.0). Index("score-plus-ten", 11.0) } type testPseudoMap struct{} func (testPseudoMap) Kind() string { return "test-pseudo-map" } func (testPseudoMap) Fields() StructMap { return testStructMap{"pseudo", 100} } func TestPseudoMap(t *testing.T) { TestValue(t, testPseudoMap{}). Repr("[^test-pseudo-map &name=pseudo &score=(num 100.0) &score-plus-ten=(num 110.0)]"). HasKey("name", "score", "score-plus-ten"). NotEqual( // Pseudo struct maps are nominally typed, so they are not equal to // maps or struct maps with the same entries. MakeMap("name", "", "score", 1.0, "score-plus-ten", 11.0), testStructMap{"ls", 1.0}, ). HasNoKey("bad", 1.0). IndexError("bad", NoSuchKey("bad")). IndexError(1.0, NoSuchKey(1.0)). AllKeys("name", "score", "score-plus-ten"). Index("name", "pseudo"). Index("score", 100.0). Index("score-plus-ten", 110.0) } elvish-0.21.0/pkg/eval/vals/tester.go000066400000000000000000000112311465720375400174110ustar00rootroot00000000000000package vals import ( "reflect" "testing" ) // Tester is a helper for testing properties of a value. type Tester struct { t *testing.T v any } // TestValue returns a ValueTester. func TestValue(t *testing.T, v any) Tester { // Hack to get test coverage on the marker method IsStructMap, which is // never invoked. if m, ok := v.(StructMap); ok { m.IsStructMap() } return Tester{t, v} } // Kind tests the Kind of the value. func (vt Tester) Kind(wantKind string) Tester { 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 Tester) Bool(wantBool bool) Tester { 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 Tester) Hash(wantHash uint32) Tester { 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 Tester) Len(wantLen int) Tester { 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 Tester) Repr(wantRepr string) Tester { vt.t.Helper() kind := ReprPlain(vt.v) 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 Tester) Equal(others ...any) Tester { 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 Tester) NotEqual(others ...any) Tester { 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 Tester) HasKey(keys ...any) Tester { 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 Tester) HasNoKey(keys ...any) Tester { 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 Tester) AllKeys(wantKeys ...any) Tester { 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 any) ([]any, error) { var keys []any err := IterateKeys(v, func(k any) 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 Tester) Index(key, wantVal any) Tester { 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 Tester) IndexError(key any, wantErr error) Tester { 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 Tester) Assoc(key, val, wantNew any) Tester { 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 Tester) AssocError(key, val any, wantErr error) Tester { 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 } elvish-0.21.0/pkg/eval/vals/testutils_test.go000066400000000000000000000004341465720375400212050ustar00rootroot00000000000000package vals import ( "src.elv.sh/pkg/tt" ) // Returns a tt.Matcher that matches using the Equal function. func eq(r any) tt.Matcher { return equalMatcher{r} } type equalMatcher struct{ want any } func (em equalMatcher) Match(got tt.RetValue) bool { return Equal(got, em.want) } elvish-0.21.0/pkg/eval/value_test.go000066400000000000000000000025221465720375400173140ustar00rootroot00000000000000package eval import ( "reflect" "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/glob" ) var reprTests = []struct { v any 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.ReprPlain(test.v) 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.21.0/pkg/eval/var_parse.go000066400000000000000000000023051465720375400171220ustar00rootroot00000000000000package 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.21.0/pkg/eval/var_parse_test.go000066400000000000000000000023351465720375400201640ustar00rootroot00000000000000package eval_test import ( "testing" . "src.elv.sh/pkg/eval" "src.elv.sh/pkg/tt" ) func TestSplitSigil(t *testing.T) { tt.Test(t, SplitSigil, 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, SplitQName, 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, SplitQNameSegs, 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, SplitIncompleteQNameNs, 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.21.0/pkg/eval/var_ref.go000066400000000000000000000122531465720375400165670ustar00rootroot00000000000000package 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, ":") { // $:foo is reserved for fully-qualified names in future return nil } 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 "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.21.0/pkg/eval/vars/000077500000000000000000000000001465720375400155645ustar00rootroot00000000000000elvish-0.21.0/pkg/eval/vars/blackhole.go000066400000000000000000000007361465720375400200450ustar00rootroot00000000000000package vars type blackhole struct{} func (blackhole) Set(any) error { return nil } func (blackhole) Get() any { 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.21.0/pkg/eval/vars/blackhole_test.go000066400000000000000000000006761465720375400211070ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args 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, IsBlackhole, Args(NewBlackhole()).Rets(true), Args(FromInit("")).Rets(false), ) } elvish-0.21.0/pkg/eval/vars/callback.go000066400000000000000000000012471465720375400176530ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/errs" ) type callback struct { set func(any) error get func() any } // FromSetGet makes a variable from a set callback and a get callback. func FromSetGet(set func(any) error, get func() any) Var { return &callback{set, get} } func (cv *callback) Set(val any) error { return cv.set(val) } func (cv *callback) Get() any { return cv.get() } type roCallback func() any // FromGet makes a variable from a get callback. The variable is read-only. func FromGet(get func() any) Var { return roCallback(get) } func (cv roCallback) Set(any) error { return errs.SetReadOnlyVar{} } func (cv roCallback) Get() any { return cv() } elvish-0.21.0/pkg/eval/vars/callback_test.go000066400000000000000000000015731465720375400207140ustar00rootroot00000000000000package vars import "testing" func TestFromSetGet(t *testing.T) { getCalled := false get := func() any { getCalled = true return "cb" } var setCalledWith any set := func(v any) 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() any { 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.21.0/pkg/eval/vars/element.go000066400000000000000000000071731465720375400175540ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/vals" ) type elem struct { variable Var assocers []any indices []any setValue any } func (ev *elem) Set(v0 any) 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() any { // 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 []any) (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([]any, 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 []any) error { var err error // In "del a[0][1][2]", // // indices: 0 1 2 // assocers: $a $a[0] // dissocer: $a[0][1] assocers := make([]any, 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.21.0/pkg/eval/vars/element_test.go000066400000000000000000000035301465720375400206040ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/eval/vals" ) var elementTests = []struct { name string oldContainer any indices []any elemValue any newContainer any }{ { "single level", vals.MakeMap("k1", "v1", "k2", "v2"), []any{"k1"}, "new v1", vals.MakeMap("k1", "new v1", "k2", "v2"), }, { "multi level", vals.MakeMap( "k1", vals.MakeMap("k1a", "v1a", "k1b", "v1b"), "k2", "v2"), []any{"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 any indices []any newContainer any }{ { "single level", vals.MakeMap("k1", "v1", "k2", "v2"), []any{"k1"}, vals.MakeMap("k2", "v2"), }, { "multi level", vals.MakeMap( "k1", vals.MakeMap("k1a", "v1a", "k1b", "v1b"), "k2", "v2"), []any{"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.ReprPlain(m), vals.ReprPlain(test.newContainer)) } }) } } elvish-0.21.0/pkg/eval/vars/env.go000066400000000000000000000012511465720375400167020ustar00rootroot00000000000000package 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 any) error { if s, ok := val.(string); ok { os.Setenv(ev.name, s) return nil } return errEnvMustBeString } func (ev envVariable) Get() any { return os.Getenv(ev.name) } func (ev envVariable) Unset() error { return os.Unsetenv(ev.name) } func (ev envVariable) IsSet() bool { _, ok := os.LookupEnv(ev.name) return ok } // FromEnv returns a Var corresponding to the named environment variable. func FromEnv(name string) UnsettableVar { return envVariable{name} } elvish-0.21.0/pkg/eval/vars/env_list.go000066400000000000000000000041501465720375400177360ustar00rootroot00000000000000package 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/errutil" "src.elv.sh/pkg/eval/vals" ) 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 any } // Get returns a Value for an EnvPathList. func (envli *envListVar) Get() any { envli.Lock() defer envli.Unlock() value := os.Getenv(envli.envName) if value == envli.cacheFor { return envli.cacheValue } envli.cacheFor = value v := vals.EmptyList for _, path := range strings.Split(value, pathListSeparator) { v = v.Conj(path) } envli.cacheValue = v return envli.cacheValue } // Set sets an EnvPathList. The underlying environment variable is set. func (envli *envListVar) Set(v any) error { var ( paths []string errElement error ) errIterate := vals.Iterate(v, func(v any) 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 errutil.Multi(errElement, errIterate) } envli.Lock() defer envli.Unlock() os.Setenv(envli.envName, strings.Join(paths, pathListSeparator)) return nil } elvish-0.21.0/pkg/eval/vars/env_test.go000066400000000000000000000014121465720375400177400ustar00rootroot00000000000000package vars import ( "os" "testing" "src.elv.sh/pkg/testutil" ) func TestEnvVariable(t *testing.T) { name := "elvish_test" testutil.Unsetenv(t, name) v := FromEnv(name).(envVariable) if set := v.IsSet(); set != false { t.Errorf("EnvVariable.Set returns true for unset env variable") } err := v.Set("foo") if err != nil || os.Getenv(name) != "foo" { t.Errorf("EnvVariable.Set doesn't alter env value") } if set := v.IsSet(); set != true { t.Errorf("EnvVariable.Set returns false for set env variable") } err = v.Set(true) if err != errEnvMustBeString { t.Errorf("envVariable.Set to a non-string value didn't return an error") } os.Setenv(name, "bar") if v.Get() != "bar" { t.Errorf("EnvVariable.Get doesn't return value set elsewhere") } } elvish-0.21.0/pkg/eval/vars/ptr.go000066400000000000000000000026611465720375400167250ustar00rootroot00000000000000package vars import ( "reflect" "sync" "src.elv.sh/pkg/eval/vals" ) type PtrVar struct { ptr any 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 any, 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 any) 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 any) Var { return FromPtr(&v) } // Get returns the value pointed by the pointer, after conversion using FromGo. func (v PtrVar) Get() any { return vals.FromGo(v.GetRaw()) } // GetRaw returns the value pointed by the pointer without any conversion. func (v PtrVar) GetRaw() any { 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 any) error { v.mutex.Lock() defer v.mutex.Unlock() return vals.ScanToGo(val, v.ptr) } elvish-0.21.0/pkg/eval/vars/ptr_test.go000066400000000000000000000014261465720375400177620ustar00rootroot00000000000000package 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.21.0/pkg/eval/vars/read_only.go000066400000000000000000000010551465720375400200700ustar00rootroot00000000000000package vars import ( "src.elv.sh/pkg/eval/errs" ) type readOnly struct { value any } // NewReadOnly creates a variable that is read-only and always returns an error // on Set. func NewReadOnly(v any) Var { return readOnly{v} } func (rv readOnly) Set(val any) error { return errs.SetReadOnlyVar{} } func (rv readOnly) Get() any { 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.21.0/pkg/eval/vars/read_only_test.go000066400000000000000000000010661465720375400211310ustar00rootroot00000000000000package vars import ( "testing" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/tt" ) 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, IsReadOnly, Args(NewReadOnly("foo")).Rets(true), Args(FromGet(func() any { return "foo" })).Rets(true), Args(FromInit("foo")).Rets(false), ) } elvish-0.21.0/pkg/eval/vars/vars.go000066400000000000000000000005021465720375400170630ustar00rootroot00000000000000// Package vars contains basic types for manipulating Elvish variables. package vars // Var represents an Elvish variable. type Var interface { Set(v any) error Get() any } // UnsettableVar represents an Elvish variable that can be in an unset state. type UnsettableVar interface { Var Unset() error IsSet() bool } elvish-0.21.0/pkg/fsutil/000077500000000000000000000000001465720375400151705ustar00rootroot00000000000000elvish-0.21.0/pkg/fsutil/claim.go000066400000000000000000000042361465720375400166110ustar00rootroot00000000000000package 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.21.0/pkg/fsutil/claim_test.go000066400000000000000000000024701465720375400176460ustar00rootroot00000000000000package 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) { testutil.InTempDir(t) testutil.ApplyDir(testutil.Dir{ "a0.log": "", "a1.log": "", "a8.log": "", "d": testutil.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) { testutil.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.21.0/pkg/fsutil/fsutil.go000066400000000000000000000001001465720375400170140ustar00rootroot00000000000000// Package fsutil provides filesystem utilities. package fsutil elvish-0.21.0/pkg/fsutil/gethome.go000066400000000000000000000015401465720375400171470ustar00rootroot00000000000000package fsutil import ( "fmt" "os" "os/user" "runtime" "strings" "src.elv.sh/pkg/env" ) // 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 != "" { if runtime.GOOS == "windows" { return strings.TrimRight(home, "/\\"), nil } else { return strings.TrimRight(home, "/"), nil } } } // Look up the user. var u *user.User var err error if uname == "" { u, err = user.Current() } 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.21.0/pkg/fsutil/getwd.go000066400000000000000000000014351465720375400166340ustar00rootroot00000000000000package fsutil import ( "os" "runtime" "strings" ) // 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+"/") || (runtime.GOOS == "windows" && strings.HasPrefix(path, home+"\\")) { return "~" + path[len(home):] } } return path } elvish-0.21.0/pkg/fsutil/getwd_test.go000066400000000000000000000027651465720375400177020ustar00rootroot00000000000000package fsutil import ( "os" "path" "path/filepath" "runtime" "testing" "src.elv.sh/pkg/env" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) func TestGetwd(t *testing.T) { tmpdir := testutil.InTempDir(t) must.OK(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) must.Chdir(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") must.Chdir(wd) must.OK(os.Remove(wd)) if gotwd := Getwd(); gotwd != "?" { t.Errorf("Getwd() -> %v, want ?", gotwd) } } } elvish-0.21.0/pkg/fsutil/search.go000066400000000000000000000026501465720375400167670ustar00rootroot00000000000000package 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 { // TODO: Remove ".." after implicit cd is removed. return exe == ".." || strings.ContainsRune(exe, filepath.Separator) || strings.ContainsRune(exe, '/') } // IsExecutable returns whether the FileInfo refers to an executable file. // // This is determined by permission bits on Unix, and by file name on Windows. func IsExecutable(stat os.FileInfo) bool { return isExecutable(stat) } // EachExternal calls f for each executable file found while scanning the directories of $E:PATH. // // NOTE: EachExternal may generate the same command multiple times; once for each time it appears in // $E:PATH. That is, no deduplication of the files found by scanning $E:PATH is performed. func EachExternal(f func(string)) { for _, dir := range searchPaths() { files, err := os.ReadDir(dir) if err != nil { // In practice this rarely happens. There isn't much we can reasonably do when it does // happen other than silently ignore the invalid directory. continue } for _, file := range files { stat, err := file.Info() if err == nil && IsExecutable(stat) { f(stat.Name()) } } } } func searchPaths() []string { return strings.Split(os.Getenv(env.PATH), string(filepath.ListSeparator)) } elvish-0.21.0/pkg/fsutil/search_unix.go000066400000000000000000000002131465720375400200230ustar00rootroot00000000000000//go:build unix package fsutil import "os" func isExecutable(stat os.FileInfo) bool { return !stat.IsDir() && stat.Mode()&0o111 != 0 } elvish-0.21.0/pkg/fsutil/search_unix_test.go000066400000000000000000000014011465720375400210620ustar00rootroot00000000000000//go:build unix package fsutil import ( "reflect" "sort" "testing" "src.elv.sh/pkg/testutil" ) 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.21.0/pkg/fsutil/search_windows.go000066400000000000000000000014731465720375400205430ustar00rootroot00000000000000package fsutil import ( "os" "path/filepath" "strings" "src.elv.sh/pkg/env" ) func isExecutable(stat os.FileInfo) bool { return !stat.IsDir() && isExecutableExt(filepath.Ext(stat.Name())) } // Determines determines a file name extension is considered executable. // It honors PATHEXT but defaults to extensions ".com", ".exe", ".bat", ".cmd" // if that env var isn't set. func isExecutableExt(ext string) bool { validExts := make(map[string]bool) if pathext := os.Getenv(env.PATHEXT); pathext != "" { for _, e := range filepath.SplitList(strings.ToLower(pathext)) { if e == "" { continue } if e[0] != '.' { e = "." + e } validExts[e] = true } } else { validExts = map[string]bool{ ".com": true, ".exe": true, ".bat": true, ".cmd": true} } return validExts[strings.ToLower(ext)] } elvish-0.21.0/pkg/fsutil/search_windows_test.go000066400000000000000000000016711465720375400216020ustar00rootroot00000000000000package fsutil import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "src.elv.sh/pkg/testutil" ) func TestEachExternal(t *testing.T) { binPath := testutil.InTempDir(t) testutil.Setenv(t, "PATH", "Z:\\foo;"+binPath+";Z:\\bar") testutil.ApplyDir(testutil.Dir{ "dir": testutil.Dir{}, "file.txt": "", "prog.bat": testutil.File{Perm: 0o666, Content: ""}, "prog.cmd": testutil.File{Perm: 0o755, Content: ""}, "prog.txt": testutil.File{Perm: 0o755, Content: ""}, "PROG.EXE": "", // validate that explicit file perms don't matter }) wantCmds := []string{"prog.bat", "prog.cmd", "PROG.EXE"} gotCmds := []string{} EachExternal(func(cmd string) { gotCmds = append(gotCmds, cmd) }) if diff := cmp.Diff(wantCmds, gotCmds, sortStringSlices); diff != "" { t.Errorf("EachExternal (-want +got): \n%s", diff) } } var sortStringSlices = cmpopts.SortSlices(func(a, b string) bool { return a < b }) elvish-0.21.0/pkg/getopt/000077500000000000000000000000001465720375400151645ustar00rootroot00000000000000elvish-0.21.0/pkg/getopt/getopt.go000066400000000000000000000217521465720375400170240ustar00rootroot00000000000000// 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,Arity,ContextType -output=zstring.go import ( "fmt" "strings" "src.elv.sh/pkg/errutil" ) // Config configurates the parsing behavior. type Config uint const ( // Stop parsing options after "--". StopAfterDoubleDash Config = 1 << iota // Stop parsing options before the first non-option argument. StopBeforeFirstNonOption // Allow long options to start with "-", and disallow short options. // Replicates the behavior of getopt_long_only and the flag package. LongOnly // Config to replicate the behavior of GNU's getopt_long. GNU = StopAfterDoubleDash // Config to replicate the behavior of BSD's getopt_long. BSD = StopAfterDoubleDash | StopBeforeFirstNonOption ) // Tests whether a configuration has all specified flags set. func (c Config) has(bits Config) bool { return c&bits == bits } // OptionSpec is a command-line option. type OptionSpec 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. Arity Arity } // Arity indicates whether an option takes an argument, and whether it is // required. type Arity uint const ( // The option takes no argument. NoArgument Arity = iota // The option requires 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 separate argument after the option (-o arg, // --long arg). RequiredArgument // The 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 ) // Option represents a parsed option. type Option struct { Spec *OptionSpec Unknown bool Long bool Argument string } // Context describes the context of the last argument. type Context struct { // The nature of the context. Type ContextType // Current option, with a likely incomplete Argument. Non-nil when Type is // OptionArgument. Option *Option // Current partial long option name or argument. Non-empty when Type is // LongOption or Argument. Text string } // ContextType encodes how the last argument can be completed. type ContextType uint const ( // OptionOrArgument indicates that the last element may be either a new // option or a new argument. Returned when it is an empty string. OptionOrArgument ContextType = iota // AnyOption indicates that the last element must be new option, short or // long. Returned when it is "-". AnyOption // 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 a non-option argument. The // partial argument is stored in Context.Text. Argument ) // Parse parses an argument list. It returns the parsed options, the non-option // arguments, and any error. func Parse(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, error) { opts, nonOptArgs, opt, _ := parse(args, specs, cfg) var err error if opt != nil { err = fmt.Errorf("missing argument for %s", optionPart(opt)) } for _, opt := range opts { if opt.Unknown { err = errutil.Multi(err, fmt.Errorf("unknown option %s", optionPart(opt))) } } return opts, nonOptArgs, err } func optionPart(opt *Option) string { if opt.Long { return "--" + opt.Spec.Long } return "-" + string(opt.Spec.Short) } // Complete parses an argument list for completion. It returns the parsed // options, the non-option arguments, and the context of the last argument. It // tolerates unknown options, assuming that they take optional arguments. func Complete(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, Context) { opts, nonOptArgs, opt, stopOpt := parse(args[:len(args)-1], specs, cfg) arg := args[len(args)-1] var ctx Context switch { case opt != nil: opt.Argument = arg ctx = Context{Type: OptionArgument, Option: opt} case stopOpt: ctx = Context{Type: Argument, Text: arg} case arg == "": ctx = Context{Type: OptionOrArgument} case arg == "-": ctx = Context{Type: AnyOption} case strings.HasPrefix(arg, "--"): if !strings.ContainsRune(arg, '=') { ctx = Context{Type: LongOption, Text: arg[2:]} } else { newopt, _ := parseLong(arg[2:], specs) ctx = Context{Type: OptionArgument, Option: newopt} } case strings.HasPrefix(arg, "-"): if cfg.has(LongOnly) { if !strings.ContainsRune(arg, '=') { ctx = Context{Type: LongOption, Text: arg[1:]} } else { newopt, _ := parseLong(arg[1:], specs) ctx = Context{Type: OptionArgument, Option: newopt} } } else { newopts, _ := parseShort(arg[1:], specs) if newopts[len(newopts)-1].Spec.Arity == NoArgument { opts = append(opts, newopts...) ctx = Context{Type: ChainShortOption} } else { opts = append(opts, newopts[:len(newopts)-1]...) ctx = Context{Type: OptionArgument, Option: newopts[len(newopts)-1]} } } default: ctx = Context{Type: Argument, Text: arg} } return opts, nonOptArgs, ctx } func parse(args []string, spec []*OptionSpec, cfg Config) ([]*Option, []string, *Option, bool) { var ( opts []*Option nonOptArgs []string // Non-nil only when the last argument was an option with required // argument, but the argument has not been seen. opt *Option // Whether option parsing has been stopped. The condition is controlled // by the StopAfterDoubleDash and StopBeforeFirstNonOption bits in cfg. stopOpt bool ) for _, arg := range args { switch { case opt != nil: opt.Argument = arg opts = append(opts, opt) opt = nil case stopOpt: nonOptArgs = append(nonOptArgs, arg) case cfg.has(StopAfterDoubleDash) && arg == "--": stopOpt = true case strings.HasPrefix(arg, "--") && arg != "--": newopt, needArg := parseLong(arg[2:], spec) if needArg { opt = newopt } else { opts = append(opts, newopt) } case strings.HasPrefix(arg, "-") && arg != "--" && arg != "-": if cfg.has(LongOnly) { newopt, needArg := parseLong(arg[1:], spec) if needArg { opt = newopt } else { opts = append(opts, newopt) } } else { newopts, needArg := parseShort(arg[1:], spec) if needArg { opts = append(opts, newopts[:len(newopts)-1]...) opt = newopts[len(newopts)-1] } else { opts = append(opts, newopts...) } } default: nonOptArgs = append(nonOptArgs, arg) if cfg.has(StopBeforeFirstNonOption) { stopOpt = true } } } return opts, nonOptArgs, opt, stopOpt } // Parses short options, without the leading dash. Returns the parsed options // and whether an argument is still to be seen. func parseShort(s string, specs []*OptionSpec) ([]*Option, bool) { var opts []*Option var needArg bool for i, r := range s { opt := findShort(r, specs) if opt != nil { if opt.Arity == NoArgument { opts = append(opts, &Option{Spec: opt}) continue } else { parsed := &Option{Spec: opt, Argument: s[i+len(string(r)):]} opts = append(opts, parsed) needArg = parsed.Argument == "" && opt.Arity == RequiredArgument break } } // Unknown option, treat as taking an optional argument parsed := &Option{ Spec: &OptionSpec{r, "", OptionalArgument}, Unknown: true, Argument: s[i+len(string(r)):]} opts = append(opts, parsed) break } return opts, needArg } func findShort(r rune, specs []*OptionSpec) *OptionSpec { for _, opt := range specs { if r == opt.Short { return opt } } return nil } // Parses a long option, without the leading dashes. Returns the parsed option // and whether an argument is still to be seen. func parseLong(s string, specs []*OptionSpec) (*Option, bool) { eq := strings.IndexRune(s, '=') for _, opt := range specs { if s == opt.Long { return &Option{Spec: opt, Long: true}, opt.Arity == RequiredArgument } else if eq != -1 && s[:eq] == opt.Long { return &Option{Spec: opt, Long: true, Argument: s[eq+1:]}, false } } // Unknown option, treat as taking an optional argument if eq == -1 { return &Option{ Spec: &OptionSpec{0, s, OptionalArgument}, Unknown: true, Long: true}, false } return &Option{ Spec: &OptionSpec{0, s[:eq], OptionalArgument}, Unknown: true, Long: true, Argument: s[eq+1:]}, false } elvish-0.21.0/pkg/getopt/getopt_test.go000066400000000000000000000205731465720375400200630ustar00rootroot00000000000000package getopt import ( "errors" "reflect" "testing" "src.elv.sh/pkg/errutil" ) var ( vSpec = &OptionSpec{'v', "verbose", NoArgument} nSpec = &OptionSpec{'n', "dry-run", NoArgument} fSpec = &OptionSpec{'f', "file", RequiredArgument} iSpec = &OptionSpec{'i', "in-place", OptionalArgument} specs = []*OptionSpec{vSpec, nSpec, fSpec, iSpec} ) var parseTests = []struct { name string cfg Config args []string wantOpts []*Option wantArgs []string wantErr error }{ { name: "short option", args: []string{"-v"}, wantOpts: []*Option{{Spec: vSpec}}, }, { name: "short option with required argument", args: []string{"-fname"}, wantOpts: []*Option{{Spec: fSpec, Argument: "name"}}, }, { name: "short option with required argument in separate argument", args: []string{"-f", "name"}, wantOpts: []*Option{{Spec: fSpec, Argument: "name"}}, }, { name: "short option with optional argument", args: []string{"-i.bak"}, wantOpts: []*Option{{Spec: iSpec, Argument: ".bak"}}, }, { name: "short option with optional argument omitted", args: []string{"-i", ".bak"}, wantOpts: []*Option{{Spec: iSpec}}, wantArgs: []string{".bak"}, }, { name: "short option chaining", args: []string{"-vn"}, wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}}, }, { name: "short option chaining with argument", args: []string{"-vfname"}, wantOpts: []*Option{{Spec: vSpec}, {Spec: fSpec, Argument: "name"}}, }, { name: "short option chaining with argument in separate argument", args: []string{"-vf", "name"}, wantOpts: []*Option{{Spec: vSpec}, {Spec: fSpec, Argument: "name"}}, }, { name: "long option", args: []string{"--verbose"}, wantOpts: []*Option{{Spec: vSpec, Long: true}}, }, { name: "long option with required argument", args: []string{"--file=name"}, wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}}, }, { name: "long option with required argument in separate argument", args: []string{"--file", "name"}, wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}}, }, { name: "long option with optional argument", args: []string{"--in-place=.bak"}, wantOpts: []*Option{{Spec: iSpec, Long: true, Argument: ".bak"}}, }, { name: "long option with optional argument omitted", args: []string{"--in-place", ".bak"}, wantOpts: []*Option{{Spec: iSpec, Long: true}}, wantArgs: []string{".bak"}, }, { name: "long option, LongOnly mode", args: []string{"-verbose"}, cfg: LongOnly, wantOpts: []*Option{{Spec: vSpec, Long: true}}, }, { name: "long option with required argument, LongOnly mode", args: []string{"-file", "name"}, cfg: LongOnly, wantOpts: []*Option{{Spec: fSpec, Long: true, Argument: "name"}}, }, { name: "StopAfterDoubleDash off", args: []string{"-v", "--", "-n"}, wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}}, wantArgs: []string{"--"}, }, { name: "StopAfterDoubleDash on", args: []string{"-v", "--", "-n"}, cfg: StopAfterDoubleDash, wantOpts: []*Option{{Spec: vSpec}}, wantArgs: []string{"-n"}, }, { name: "StopBeforeFirstNonOption off", args: []string{"-v", "foo", "-n"}, wantOpts: []*Option{{Spec: vSpec}, {Spec: nSpec}}, wantArgs: []string{"foo"}, }, { name: "StopBeforeFirstNonOption on", args: []string{"-v", "foo", "-n"}, cfg: StopBeforeFirstNonOption, wantOpts: []*Option{{Spec: vSpec}}, wantArgs: []string{"foo", "-n"}, }, { name: "single dash is not an option", args: []string{"-"}, wantArgs: []string{"-"}, }, { name: "single dash is not an option, LongOnly mode", args: []string{"-"}, cfg: LongOnly, wantArgs: []string{"-"}, }, { name: "short option with required argument missing", args: []string{"-f"}, wantErr: errors.New("missing argument for -f"), }, { name: "long option with required argument missing", args: []string{"--file"}, wantErr: errors.New("missing argument for --file"), }, { name: "unknown short option", args: []string{"-b"}, wantOpts: []*Option{ {Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true}}, wantErr: errors.New("unknown option -b"), }, { name: "unknown short option with argument", args: []string{"-bfoo"}, wantOpts: []*Option{ {Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true, Argument: "foo"}}, wantErr: errors.New("unknown option -b"), }, { name: "unknown long option", args: []string{"--bad"}, wantOpts: []*Option{ {Spec: &OptionSpec{Long: "bad", Arity: OptionalArgument}, Long: true, Unknown: true}}, wantErr: errors.New("unknown option --bad"), }, { name: "unknown long option with argument", args: []string{"--bad=foo"}, wantOpts: []*Option{ {Spec: &OptionSpec{Long: "bad", Arity: OptionalArgument}, Long: true, Unknown: true, Argument: "foo"}}, wantErr: errors.New("unknown option --bad"), }, { name: "multiple errors", args: []string{"-b", "-f"}, wantOpts: []*Option{ {Spec: &OptionSpec{Short: 'b', Arity: OptionalArgument}, Unknown: true}}, wantErr: errutil.Multi( errors.New("missing argument for -f"), errors.New("unknown option -b")), }, } func TestParse(t *testing.T) { for _, tc := range parseTests { t.Run(tc.name, func(t *testing.T) { opts, args, err := Parse(tc.args, specs, tc.cfg) check := func(name string, got, want any) { if !reflect.DeepEqual(got, want) { t.Errorf("Parse(%#v) (config = %v)\ngot %s = %v, want %v", tc.args, tc.cfg, name, got, want) } } check("opts", opts, tc.wantOpts) check("args", args, tc.wantArgs) check("err", err, tc.wantErr) }) } } var completeTests = []struct { name string cfg Config args []string wantOpts []*Option wantArgs []string wantCtx Context }{ { name: "NewOptionOrArgument", args: []string{""}, wantCtx: Context{Type: OptionOrArgument}, }, { name: "NewOption", args: []string{"-"}, wantCtx: Context{Type: AnyOption}, }, { name: "LongOption", args: []string{"--f"}, wantCtx: Context{Type: LongOption, Text: "f"}, }, { name: "LongOption with LongOnly", args: []string{"-f"}, cfg: LongOnly, wantCtx: Context{Type: LongOption, Text: "f"}, }, { name: "ChainShortOption", args: []string{"-v"}, wantOpts: []*Option{{Spec: vSpec}}, wantCtx: Context{Type: ChainShortOption}, }, { name: "OptionArgument of short option, separate argument", args: []string{"-f", "foo"}, wantCtx: Context{ Type: OptionArgument, Option: &Option{Spec: fSpec, Argument: "foo"}}, }, { name: "OptionArgument of short option, same argument", args: []string{"-ffoo"}, wantCtx: Context{ Type: OptionArgument, Option: &Option{Spec: fSpec, Argument: "foo"}}, }, { name: "OptionArgument of long option, separate argument", args: []string{"--file", "foo"}, wantCtx: Context{ Type: OptionArgument, Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}}, }, { name: "OptionArgument of long option, same argument", args: []string{"--file=foo"}, wantCtx: Context{ Type: OptionArgument, Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}}, }, { name: "OptionArgument of long option with LongOnly, same argument", args: []string{"-file=foo"}, cfg: LongOnly, wantCtx: Context{ Type: OptionArgument, Option: &Option{Spec: fSpec, Long: true, Argument: "foo"}}, }, { name: "Argument", args: []string{"foo"}, wantCtx: Context{Type: Argument, Text: "foo"}, }, { name: "Argument after --", args: []string{"--", "foo"}, cfg: StopAfterDoubleDash, wantCtx: Context{Type: Argument, Text: "foo"}, }, { name: "Argument after first non-option argument", args: []string{"bar", "foo"}, cfg: StopBeforeFirstNonOption, wantArgs: []string{"bar"}, wantCtx: Context{Type: Argument, Text: "foo"}, }, } func TestComplete(t *testing.T) { for _, tc := range completeTests { t.Run(tc.name, func(t *testing.T) { opts, args, ctx := Complete(tc.args, specs, tc.cfg) check := func(name string, got, want any) { if !reflect.DeepEqual(got, want) { t.Errorf("Parse(%#v) (config = %v)\ngot %s = %v, want %v", tc.args, tc.cfg, name, got, want) } } check("opts", opts, tc.wantOpts) check("args", args, tc.wantArgs) check("ctx", ctx, tc.wantCtx) }) } } elvish-0.21.0/pkg/getopt/zstring.go000066400000000000000000000040421465720375400172130ustar00rootroot00000000000000// Code generated by "stringer -type=Config,Arity,ContextType -output=zstring.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[StopAfterDoubleDash-1] _ = x[StopBeforeFirstNonOption-2] _ = x[LongOnly-4] } const ( _Config_name_0 = "StopAfterDoubleDashStopBeforeFirstNonOption" _Config_name_1 = "LongOnly" ) var ( _Config_index_0 = [...]uint8{0, 19, 43} ) 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 _Arity_name = "NoArgumentRequiredArgumentOptionalArgument" var _Arity_index = [...]uint8{0, 10, 26, 42} func (i Arity) String() string { if i >= Arity(len(_Arity_index)-1) { return "Arity(" + strconv.FormatInt(int64(i), 10) + ")" } return _Arity_name[_Arity_index[i]:_Arity_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[OptionOrArgument-0] _ = x[AnyOption-1] _ = x[LongOption-2] _ = x[ChainShortOption-3] _ = x[OptionArgument-4] _ = x[Argument-5] } const _ContextType_name = "OptionOrArgumentAnyOptionLongOptionChainShortOptionOptionArgumentArgument" var _ContextType_index = [...]uint8{0, 16, 25, 35, 51, 65, 73} 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.21.0/pkg/glob/000077500000000000000000000000001465720375400146055ustar00rootroot00000000000000elvish-0.21.0/pkg/glob/glob.go000066400000000000000000000210601465720375400160560ustar00rootroot00000000000000// 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. Files that // can't be lstat'ed and directories that can't be read are ignored silently. 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. // It is also required for handling directory components that are actually // symbolic links to directories. for len(segs) > 1 && IsLiteral(segs[0]) && IsSlash(segs[1]) { elem := segs[0].(Literal).Data segs = segs[2:] dir += elem + "/" // This will correctly resolve symbolic links when they appear literally // (e.g. in "link-to-dir/*") despite the use of Lstat, since a trailing // slash always causes symbolic links to be resolved // (https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13). if info, err := os.Lstat(dir); err != nil || !info.IsDir() { return true } } if len(segs) == 0 { if info, err := os.Lstat(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.Lstat(path); err == nil { return cb(PathInfo{path, info}) } return true } infos, err := readDir(dir) if err != nil { // Ignore directories that can't be read. 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) { fullname := dir + name info, err := os.Lstat(fullname) if err != nil { // Either the file was removed between ReadDir and Lstat, or the // OS has some special rule that prevents it from being lstat'ed // (see b.elv.sh/1674 for a known case on macOS; SELinux and // FreeBSD's MAC might be able to do the same). In either case, // ignore the file. continue } if !cb(PathInfo{fullname, 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 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.21.0/pkg/glob/glob_test.go000066400000000000000000000104021465720375400171130ustar00rootroot00000000000000package 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"} symlinks = []struct { path string target string }{ {"d1/s-f", "f"}, {"s-d", "d2"}, {"s-d-f", "d1/f"}, {"s-bad", "bad"}, } ) type globCase struct { pattern string want []string } var globCases = []globCase{ {"*", []string{"a", "b", "c", "d1", "d2", "dX", "dXY", "lorem", "ipsum", "s-bad", "s-d", "s-d-f"}}, {".", []string{"."}}, {"./*", []string{"./a", "./b", "./c", "./d1", "./d2", "./dX", "./dXY", "./lorem", "./ipsum", "./s-bad", "./s-d", "./s-d-f"}}, {"..", []string{".."}}, {"a/..", []string{"a/.."}}, {"a/../*", []string{"a/../a", "a/../b", "a/../c", "a/../d1", "a/../d2", "a/../dX", "a/../dXY", "a/../ipsum", "a/../lorem", "a/../s-bad", "a/../s-d", "a/../s-d-f"}}, {"*/", []string{"a/", "b/", "c/", "d1/", "d2/"}}, {"**", []string{"a", "a/X", "a/Y", "b", "b/X", "c", "c/Y", "d1", "d1/e", "d1/e/f", "d1/e/f/g", "d1/e/f/g/X", "d1/s-f", "d2", "d2/e", "d2/e/f", "d2/e/f/g", "d2/e/f/g/X", "dX", "dXY", "ipsum", "lorem", "s-bad", "s-d", "s-d-f"}}, {"*/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", "d1/s-f", "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: Add more tests for situations where Lstat fails. // 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 _, 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, create the symlink as an // ordinary file instead. f, err := os.Create(link.path) 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.21.0/pkg/glob/parse.go000066400000000000000000000026251465720375400162530ustar00rootroot00000000000000package 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.21.0/pkg/glob/parse_test.go000066400000000000000000000020101465720375400172760ustar00rootroot00000000000000package 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.21.0/pkg/glob/pattern.go000066400000000000000000000032131465720375400166100ustar00rootroot00000000000000package 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.21.0/pkg/logutil/000077500000000000000000000000001465720375400153415ustar00rootroot00000000000000elvish-0.21.0/pkg/logutil/logutil.go000066400000000000000000000024421465720375400173510ustar00rootroot00000000000000// 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 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.21.0/pkg/logutil/logutil_test.go000066400000000000000000000016021465720375400204050ustar00rootroot00000000000000package logutil import ( "os" "path/filepath" "regexp" "testing" "src.elv.sh/pkg/must" ) func TestLogger(t *testing.T) { logger := GetLogger("foo ") r, w := must.Pipe() SetOutput(w) logger.Println("out 1") w.Close() wantOut1 := must.OK1(regexp.Compile("^foo .*out 1\n$")) if out := must.ReadAllAndClose(r); !wantOut1.Match(out) { t.Errorf("got out %q, want one matching %q", out, wantOut1) } outPath := filepath.Join(t.TempDir(), "out") must.OK(SetOutputFile(outPath)) logger.Println("out 2") must.OK(SetOutputFile("")) wantOut2 := must.OK1(regexp.Compile("^foo .*out 2\n$")) if out := must.ReadAllAndClose(must.OK1(os.Open(outPath))); !wantOut2.Match(out) { t.Errorf("got out %q, want one matching %q", out, wantOut2) } } func TestSetOutput_Error(t *testing.T) { err := SetOutputFile("/bad/file/path") if err == nil { t.Errorf("want non-nil error, got nil") } } elvish-0.21.0/pkg/lsp/000077500000000000000000000000001465720375400144605ustar00rootroot00000000000000elvish-0.21.0/pkg/lsp/lsp.go000066400000000000000000000020151465720375400156030ustar00rootroot00000000000000// Package lsp implements a language server for Elvish. package lsp import ( "context" "os" "github.com/sourcegraph/jsonrpc2" "src.elv.sh/pkg/prog" ) // Program is the LSP subprogram. type Program struct { run bool } func (p *Program) RegisterFlags(fs *prog.FlagSet) { fs.BoolVar(&p.run, "lsp", false, "Run the builtin language server") } func (p *Program) Run(fds [3]*os.File, _ []string) error { if !p.run { return prog.NextProgram() } ctx, cancel := context.WithCancel(context.Background()) defer cancel() s := newServer() conn := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(transport{fds[0], fds[1]}, jsonrpc2.VSCodeObjectCodec{}), handler(s)) <-conn.DisconnectNotify() return nil } type transport struct{ in, out *os.File } func (c transport) Read(p []byte) (int, error) { return c.in.Read(p) } func (c transport) Write(p []byte) (int, error) { return c.out.Write(p) } func (c transport) Close() error { if err := c.in.Close(); err != nil { c.out.Close() return err } return c.out.Close() } elvish-0.21.0/pkg/lsp/lsp_test.elvts000066400000000000000000000003111465720375400173670ustar00rootroot00000000000000//each:elvish-in-global /////////////////////////////////// # return NextProgram without -lsp # /////////////////////////////////// ~> elvish [stderr] internal error: no suitable subprogram [exit] 2 elvish-0.21.0/pkg/lsp/lsp_test.go000066400000000000000000000211231465720375400166430ustar00rootroot00000000000000package lsp import ( "context" "encoding/json" "fmt" "os" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/sourcegraph/jsonrpc2" lsp "pkg.nimblebun.works/go-lsp" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/must" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/testutil" ) var bgCtx = context.Background() var diagTests = []struct { name string text string wantDiags []lsp.Diagnostic }{ {"empty", "", []lsp.Diagnostic{}}, {"no error", "echo", []lsp.Diagnostic{}}, {"single error", "$!", []lsp.Diagnostic{ { Range: lsp.Range{ Start: lsp.Position{Line: 0, Character: 1}, End: lsp.Position{Line: 0, Character: 2}}, Severity: lsp.DSError, Source: "parse", Message: "should be variable name", }, }}, {"multi line with NL", "\n$!", []lsp.Diagnostic{ { Range: lsp.Range{ Start: lsp.Position{Line: 1, Character: 1}, End: lsp.Position{Line: 1, Character: 2}}, Severity: lsp.DSError, Source: "parse", Message: "should be variable name", }, }}, {"multi line with CR", "\r$!", []lsp.Diagnostic{ { Range: lsp.Range{ Start: lsp.Position{Line: 1, Character: 1}, End: lsp.Position{Line: 1, Character: 2}}, Severity: lsp.DSError, Source: "parse", Message: "should be variable name", }, }}, {"multi line with CRNL", "\r\n$!", []lsp.Diagnostic{ { Range: lsp.Range{ Start: lsp.Position{Line: 1, Character: 1}, End: lsp.Position{Line: 1, Character: 2}}, Severity: lsp.DSError, Source: "parse", Message: "should be variable name", }, }}, {"text with code point beyond FFFF", "\U00010000 $!", []lsp.Diagnostic{ { Range: lsp.Range{ Start: lsp.Position{Line: 0, Character: 4}, End: lsp.Position{Line: 0, Character: 5}}, Severity: lsp.DSError, Source: "parse", Message: "should be variable name", }, }}, } func TestDidOpenDiagnostics(t *testing.T) { f := setup(t) for _, test := range diagTests { t.Run(test.name, func(t *testing.T) { f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams(test.text)) checkDiag(t, f, diagParam(test.wantDiags)) }) } } func TestDidChangeDiagnostics(t *testing.T) { f := setup(t) f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams("")) checkDiag(t, f, diagParam([]lsp.Diagnostic{})) for _, test := range diagTests { t.Run(test.name, func(t *testing.T) { f.conn.Notify(bgCtx, "textDocument/didChange", didChangeParams(test.text)) checkDiag(t, f, diagParam(test.wantDiags)) }) } } var hoverTests = []struct { name string text string pos lsp.Position wantHover lsp.Hover }{ { name: "command doc", text: "echo foo", pos: lsp.Position{Line: 0, Character: 0}, wantHover: hoverWith(must.OK1(doc.Source("echo"))), }, { name: "variable doc", // 012345 text: "echo $paths", pos: lsp.Position{Line: 0, Character: 5}, wantHover: hoverWith(must.OK1(doc.Source("$paths"))), }, { name: "unknown command", text: "some-external", pos: lsp.Position{Line: 0, Character: 0}, wantHover: lsp.Hover{}, }, { name: "command at non-command position", // 012345678 text: "echo echo", pos: lsp.Position{Line: 0, Character: 5}, wantHover: lsp.Hover{}, }, } func hoverWith(markdown string) lsp.Hover { return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}} } func TestHover(t *testing.T) { f := setup(t) for _, test := range hoverTests { t.Run(test.name, func(t *testing.T) { f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams(test.text)) request := lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: testURI}, Position: test.pos, } var response lsp.Hover err := f.conn.Call(bgCtx, "textDocument/hover", request, &response) if err != nil { t.Errorf("got error %v", err) } if diff := cmp.Diff(test.wantHover, response); diff != "" { t.Errorf("response (-want +got):\n%s", diff) } }) } } var completionTests = []struct { name string text string params lsp.CompletionParams wantKind lsp.CompletionItemKind }{ {"command", "", completionParams(0, 0), lsp.CIKFunction}, {"variable", "put $", completionParams(0, 5), lsp.CIKVariable}, {"bad", "put [", completionParams(0, 5), 0}, } func TestCompletion(t *testing.T) { f := setup(t) testutil.Setenv(t, "PATH", "") for _, test := range completionTests { t.Run(test.name, func(t *testing.T) { var items []lsp.CompletionItem f.conn.Notify(bgCtx, "textDocument/didOpen", didOpenParams(test.text)) err := f.conn.Call(bgCtx, "textDocument/completion", test.params, &items) if err != nil { t.Errorf("got error %v", err) } if test.wantKind == 0 { if len(items) > 0 { t.Errorf("got %v items, want 0", len(items)) } } else { if len(items) == 0 { t.Fatalf("got 0 items, want non-zero") } if items[0].Kind != test.wantKind { t.Errorf("got kind %v, want %v", items[0].Kind, test.wantKind) } } }) } } var jsonrpcErrorTests = []struct { name string method string params any wantErr error }{ {"unknown method", "unknown/method", struct{}{}, errMethodNotFound}, {"invalid request type", "textDocument/didOpen", []int{}, errInvalidParams}, {"unknown document to hover", "textDocument/hover", lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: "file://unknown"}}, unknownDocument("file://unknown")}, {"unknown document to completion", "textDocument/completion", lsp.CompletionParams{ TextDocumentPositionParams: lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: "file://unknown"}}}, unknownDocument("file://unknown")}, } func TestJSONRPCErrors(t *testing.T) { f := setup(t) for _, test := range jsonrpcErrorTests { t.Run(test.name, func(t *testing.T) { err := f.conn.Call(context.Background(), test.method, test.params, &struct{}{}) if err.Error() != test.wantErr.Error() { t.Errorf("got error %v, want %v", err, test.wantErr) } }) } } const testURI = "file:///foo" func didOpenParams(text string) lsp.DidOpenTextDocumentParams { return lsp.DidOpenTextDocumentParams{ TextDocument: lsp.TextDocumentItem{URI: testURI, Text: text}} } func didChangeParams(text string) lsp.DidChangeTextDocumentParams { return lsp.DidChangeTextDocumentParams{ TextDocument: lsp.VersionedTextDocumentIdentifier{ TextDocumentIdentifier: lsp.TextDocumentIdentifier{URI: testURI}, }, ContentChanges: []lsp.TextDocumentContentChangeEvent{ {Text: text}, }} } func diagParam(diags []lsp.Diagnostic) lsp.PublishDiagnosticsParams { return lsp.PublishDiagnosticsParams{URI: testURI, Diagnostics: diags} } func checkDiag(t *testing.T, f *clientFixture, want lsp.PublishDiagnosticsParams) { t.Helper() select { case got := <-f.diags: if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } case <-time.After(testutil.Scaled(time.Second)): t.Errorf("time out") } } func completionParams(line, char int) lsp.CompletionParams { return lsp.CompletionParams{ TextDocumentPositionParams: lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: testURI}, Position: lsp.Position{Line: line, Character: char}, }, } } type clientFixture struct { conn *jsonrpc2.Conn diags <-chan lsp.PublishDiagnosticsParams } func setup(t *testing.T) *clientFixture { r0, w0 := must.Pipe() r1, w1 := must.Pipe() // Run server done := make(chan struct{}) go func() { prog.Run([3]*os.File{r0, w1, nil}, []string{"elvish", "-lsp"}, &Program{}) close(done) }() t.Cleanup(func() { <-done }) // Run client diags := make(chan lsp.PublishDiagnosticsParams, 100) client := client{diags} conn := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(transport{r1, w0}, jsonrpc2.VSCodeObjectCodec{}), client.handler()) t.Cleanup(func() { conn.Close() }) // LSP handshake err := conn.Call(context.Background(), "initialize", lsp.InitializeParams{}, &lsp.InitializeResult{}) if err != nil { t.Errorf("got error %v, want nil", err) } err = conn.Notify(context.Background(), "initialized", struct{}{}) if err != nil { t.Errorf("got error %v, want nil", err) } return &clientFixture{conn, diags} } type client struct { diags chan<- lsp.PublishDiagnosticsParams } func (c *client) handler() jsonrpc2.Handler { return routingHandler(map[string]method{ "textDocument/publishDiagnostics": c.publishDiagnostics, }) } func (c *client) publishDiagnostics(_ context.Context, rawParams json.RawMessage) (any, error) { var params lsp.PublishDiagnosticsParams err := json.Unmarshal(rawParams, ¶ms) if err != nil { panic(fmt.Sprintf("parse PublishDiagnosticsParams: %v", err)) } c.diags <- params return nil, nil } elvish-0.21.0/pkg/lsp/server.go000066400000000000000000000166151465720375400163260ustar00rootroot00000000000000package lsp import ( "context" "encoding/json" "fmt" "github.com/sourcegraph/jsonrpc2" lsp "pkg.nimblebun.works/go-lsp" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/edit/complete" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/parse/np" ) var ( errMethodNotFound = &jsonrpc2.Error{ Code: jsonrpc2.CodeMethodNotFound, Message: "method not found"} errInvalidParams = &jsonrpc2.Error{ Code: jsonrpc2.CodeInvalidParams, Message: "invalid params"} ) type server struct { evaler *eval.Evaler documents map[lsp.DocumentURI]document } type document struct { code string parseTree parse.Tree parseErr error } func newServer() *server { return &server{eval.NewEvaler(), make(map[lsp.DocumentURI]document)} } func handler(s *server) jsonrpc2.Handler { return routingHandler(map[string]method{ "initialize": s.initialize, "textDocument/didOpen": convertMethod(s.didOpen), "textDocument/didChange": convertMethod(s.didChange), "textDocument/hover": convertMethod(s.hover), "textDocument/completion": convertMethod(s.completion), "textDocument/didClose": noop, // Required by spec. "initialized": noop, // Called by clients even when server doesn't advertise support: // https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles "workspace/didChangeWatchedFiles": noop, }) } type method func(context.Context, json.RawMessage) (any, error) func convertMethod[T any](f func(context.Context, T) (any, error)) method { return func(ctx context.Context, rawParams json.RawMessage) (any, error) { var params T if json.Unmarshal(rawParams, ¶ms) != nil { return nil, errInvalidParams } return f(ctx, params) } } func noop(_ context.Context, _ json.RawMessage) (any, error) { return nil, nil } type connKey struct{} func routingHandler(methods map[string]method) jsonrpc2.Handler { return jsonrpc2.HandlerWithError(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (any, error) { fn, ok := methods[req.Method] if !ok { return nil, errMethodNotFound } return fn(context.WithValue(ctx, connKey{}, conn), *req.Params) }) } // Can be used within handler implementations to recover the connection stored // in the Context. func conn(ctx context.Context) *jsonrpc2.Conn { return ctx.Value(connKey{}).(*jsonrpc2.Conn) } // Handler implementations. These are all called synchronously. func (s *server) initialize(_ context.Context, _ json.RawMessage) (any, error) { return &lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ TextDocumentSync: &lsp.TextDocumentSyncOptions{ OpenClose: true, Change: lsp.TDSyncKindFull, }, CompletionProvider: &lsp.CompletionOptions{}, HoverProvider: &lsp.HoverOptions{}, }, }, nil } func (s *server) didOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) (any, error) { uri, content := params.TextDocument.URI, params.TextDocument.Text s.updateDocument(conn(ctx), uri, content) return nil, nil } func (s *server) didChange(ctx context.Context, params lsp.DidChangeTextDocumentParams) (any, error) { // ContentChanges includes full text since the server is only advertised to // support that; see the initialize method. uri, content := params.TextDocument.URI, params.ContentChanges[0].Text s.updateDocument(conn(ctx), uri, content) return nil, nil } func (s *server) hover(_ context.Context, params lsp.TextDocumentPositionParams) (any, error) { document, ok := s.documents[params.TextDocument.URI] if !ok { return nil, unknownDocument(params.TextDocument.URI) } pos := lspPositionToIdx(document.code, params.Position) p := np.Find(document.parseTree.Root, pos) // Try variable doc var primary *parse.Primary if p.Match(np.Store(&primary)) && primary.Type == parse.Variable { // TODO: Take shadowing into consideration. markdown, err := doc.Source("$" + primary.Value) if err == nil { return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}}, nil } } // Try command doc var expr np.SimpleExprData var form *parse.Form if p.Match(np.SimpleExpr(&expr, nil), np.Store(&form)) && form.Head == expr.Compound { // TODO: Take shadowing into consideration. markdown, err := doc.Source(expr.Value) if err == nil { return lsp.Hover{Contents: lsp.MarkupContent{Kind: lsp.MKMarkdown, Value: markdown}}, nil } } return nil, nil } func (s *server) completion(_ context.Context, params lsp.CompletionParams) (any, error) { document, ok := s.documents[params.TextDocument.URI] if !ok { return nil, unknownDocument(params.TextDocument.URI) } code := document.code result, err := complete.Complete( complete.CodeBuffer{ Content: code, Dot: lspPositionToIdx(code, params.Position)}, s.evaler, complete.Config{}, ) if err != nil { return []lsp.CompletionItem{}, nil } lspItems := make([]lsp.CompletionItem, len(result.Items)) lspRange := lspRangeFromRange(code, result.Replace) var kind lsp.CompletionItemKind switch result.Name { case "command": kind = lsp.CIKFunction case "variable": kind = lsp.CIKVariable default: // TODO: Support more values of kind } for i, item := range result.Items { lspItems[i] = lsp.CompletionItem{ Label: item.ToInsert, Kind: kind, TextEdit: &lsp.TextEdit{ Range: lspRange, NewText: item.ToInsert, }, } } return lspItems, nil } func (s *server) updateDocument(conn *jsonrpc2.Conn, uri lsp.DocumentURI, code string) { tree, err := parse.Parse(parse.Source{Name: string(uri), Code: code}, parse.Config{}) s.documents[uri] = document{code, tree, err} go func() { // Convert the parse error to lsp.Diagnostic objects and publish them. entries := parse.UnpackErrors(err) diags := make([]lsp.Diagnostic, len(entries)) for i, err := range entries { diags[i] = lsp.Diagnostic{ Range: lspRangeFromRange(code, err), Severity: lsp.DSError, Source: "parse", Message: err.Message, } } conn.Notify(context.Background(), "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{URI: uri, Diagnostics: diags}) }() } func unknownDocument(uri lsp.DocumentURI) error { return &jsonrpc2.Error{ Code: jsonrpc2.CodeInvalidParams, Message: fmt.Sprintf("unknown document: %v", uri), } } func lspRangeFromRange(s string, r diag.Ranger) lsp.Range { rg := r.Range() return lsp.Range{ Start: lspPositionFromIdx(s, rg.From), End: lspPositionFromIdx(s, rg.To), } } func lspPositionToIdx(s string, pos lsp.Position) int { var idx int walkString(s, func(i int, p lsp.Position) bool { idx = i return p.Line < pos.Line || (p.Line == pos.Line && p.Character < pos.Character) }) return idx } func lspPositionFromIdx(s string, idx int) lsp.Position { var pos lsp.Position walkString(s, func(i int, p lsp.Position) bool { pos = p return i < idx }) return pos } // Generates (index, lspPosition) pairs in s, stopping if f returns false. func walkString(s string, f func(i int, p lsp.Position) bool) { var p lsp.Position lastCR := false for i, r := range s { if !f(i, p) { return } switch { case r == '\r': p.Line++ p.Character = 0 case r == '\n': if lastCR { // Ignore \n if it's part of a \r\n sequence } else { p.Line++ p.Character = 0 } case r <= 0xFFFF: // Encoded in UTF-16 with one unit p.Character++ default: // Encoded in UTF-16 with two units p.Character += 2 } lastCR = r == '\r' } f(len(s), p) } elvish-0.21.0/pkg/lsp/transcripts_test.go000066400000000000000000000005151465720375400204230ustar00rootroot00000000000000package lsp_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/lsp" "src.elv.sh/pkg/prog/progtest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvish-in-global", progtest.ElvishInGlobal(&lsp.Program{}), ) } elvish-0.21.0/pkg/md/000077500000000000000000000000001465720375400142625ustar00rootroot00000000000000elvish-0.21.0/pkg/md/fmt.go000066400000000000000000001027471465720375400154120ustar00rootroot00000000000000package md import ( "fmt" "regexp" "strconv" "strings" "unicode" "unicode/utf8" "src.elv.sh/pkg/wcwidth" ) // FmtCodec is a codec that formats Markdown in a specific style. // // The only supported configuration option is the text width. // // The formatted text uses the following style: // // - Blocks are always separated by a blank line. // // - Thematic breaks use "***" where possible, falling back to "---" if using // the former is problematic. // // - Code blocks are always fenced, never indented. // // - Code fences use backquotes (like "```") wherever possible, falling back // to "~~~" if using the former is problematic. // // - Continuation markers of container blocks ("> " for blockquotes and spaces // for list items) are never omitted; in other words, lazy continuation is // never used. // // - Blockquotes use "> ", never omitting the space. // // - Bullet lists use "-" as markers where possible, falling back to "*" if // using the former is problematic. // // - Ordered lists use "X." (X being a number) where possible, falling back to // "X)" if using the former is problematic. // // - Bullet lists and ordered lists are indented 4 spaces where possible. // // - Emphasis always uses "*". // // - Strong emphasis always uses "**". // // - Hard line break always uses an explicit "\". type FmtCodec struct { Width int sb strings.Builder unsupported *FmtUnsupported // Current active container blocks. containers stack[*fmtContainer] // The value of sb.Len() when the last container block was started. Used to // determine whether a container is empty, in which case a blank line is // needed to preserve the container. containerStart int // The punctuation of the just popped list container, only populated if the // last Op was OpBulletListEnd or OpOrderedListEnd. Used to alternate list // punctuation when a list follows directly after another of the same type. poppedListPunct rune // Value of op.Type of the last Do call. lastOpType OpType } // FmtUnsupported contains information about use of unsupported features. type FmtUnsupported struct { // Input contains emphasis or strong emphasis nested in another emphasis or // strong emphasis (not necessarily of the same type). NestedEmphasisOrStrongEmphasis bool // Input contains emphasis or strong emphasis that follows immediately after // another emphasis or strong emphasis (not necessarily of the same type). ConsecutiveEmphasisOrStrongEmphasis bool } func (c *FmtCodec) String() string { return c.sb.String() } // Unsupported returns information about use of unsupported features that may // make the output incorrect. It returns nil if there is no use of unsupported // features. func (c *FmtCodec) Unsupported() *FmtUnsupported { return c.unsupported } func (c *FmtCodec) setUnsupported() *FmtUnsupported { if c.unsupported == nil { c.unsupported = &FmtUnsupported{} } return c.unsupported } var ( backquoteRunRegexp = regexp.MustCompile("`+") tildeRunRegexp = regexp.MustCompile("~+") ) func (c *FmtCodec) Do(op Op) { var poppedListPunct rune defer func() { c.poppedListPunct = poppedListPunct }() if c.sb.Len() > 0 && needNewStanza(op.Type, c.lastOpType) { c.writeLine("") } defer func() { c.lastOpType = op.Type }() switch op.Type { case OpThematicBreak: if len(c.containers) > 0 && strings.TrimSpace(c.containers[len(c.containers)-1].marker) == "*" { // If the last marker to write is "*", using "***" will swallow the // marker. c.writeLine("---") } else { c.writeLine("***") } case OpHeading: c.startLine() c.write(strings.Repeat("#", op.Number) + " ") c.writeSegmentsATXHeading(c.buildSegments(op.Content)) if op.Info != "" { c.write(" {" + op.Info + "}") } c.finishLine() case OpCodeBlock: startFence, endFence := codeFences(op.Info, op.Lines) c.writeLine(startFence) for _, line := range op.Lines { c.writeLine(line) } c.writeLine(endFence) case OpHTMLBlock: if c.lastOpType == OpListItemStart && strings.HasPrefix(op.Lines[0], " ") { // HTML blocks can contain 1 to 3 leading spaces. When it appears at // the first line of a list item, following "- " or "* ", those // spaces will either get merged into the list marker (in case of 1 // leading space) or turn the HTML block into an indented code block // (in case of 2 or 3 leading spaces). // // To fix this, use a blank line as the first line, and start the // HTML block on the second line. The marker needs to be shortened // to contain exactly one trailing space, as is required by rule 3 // in https://spec.commonmark.org/0.31.2/#list-items. // // Note that this only matters for HTML blocks. Indented code blocks // has the same behavior regarding leading spaces, but we always // turn them into fenced code blocks, moving the content to the // second line and avoiding this problem. Other types of blocks // either don't allow leading spaces, or don't preserve them. lastMarker := &c.containers[len(c.containers)-1].marker *lastMarker = strings.TrimRight(*lastMarker, " ") + " " c.writeLine("") } for _, line := range op.Lines { c.writeLine(line) } case OpParagraph: c.startLine() segs := c.buildSegments(op.Content) if c.Width > 0 { c.writeSegmentsParagraphReflow(segs, c.Width) } else { c.writeSegmentsParagraph(segs) } c.finishLine() case OpBlockquoteStart: c.containerStart = c.sb.Len() c.containers.push(&fmtContainer{typ: fmtBlockquote, marker: "> "}) case OpBlockquoteEnd: if c.containerStart == c.sb.Len() { c.writeLine("") } c.containers.pop() case OpListItemStart: c.containerStart = c.sb.Len() // Set marker to start marker if ct := c.containers.peek(); ct.typ == fmtBulletItem { ct.marker = fmt.Sprintf("%c ", ct.punct) } else { ct.marker = fmt.Sprintf("%d%c ", ct.number, ct.punct) if len(ct.marker) < 4 { ct.marker += strings.Repeat(" ", 4-len(ct.marker)) } } case OpListItemEnd: if c.containerStart == c.sb.Len() { // When a list item is empty, we will write a line consisting of // bullet punctuations and spaces only. When there are at least 3 // instances of the same punctuation, this line will be become a // thematic break instead. Avoid this by varying the punctuation. // // We use "-" whenever possible. If there are 3 consecutive // identical starter marks, they can only be all "- ". for i := 2; i < len(c.containers); i++ { ct := c.containers[i] if allDashBullets(c.containers[i-2 : i+1]) { ct.punct = pickPunct('-', '*', ct.punct) ct.marker = fmt.Sprintf("%c ", ct.punct) } } c.writeLine("") } ct := c.containers.peek() ct.marker = "" // If the current number is 9 9's, incrementing it will make the number // 10 digits; CommonMark requires the number in the ordered list to be // at most 9 digits. So just stop incrementing at this number. if ct.number < 999999999 { ct.number++ } case OpBulletListStart: c.containers.push(&fmtContainer{ typ: fmtBulletItem, punct: pickPunct('-', '*', c.poppedListPunct)}) case OpBulletListEnd: poppedListPunct = c.containers.pop().punct case OpOrderedListStart: c.containers.push(&fmtContainer{ typ: fmtOrderedItem, punct: pickPunct('.', ')', c.poppedListPunct), number: op.Number}) case OpOrderedListEnd: poppedListPunct = c.containers.pop().punct } } func needNewStanza(cur, last OpType) bool { switch cur { case OpThematicBreak, OpHeading, OpCodeBlock, OpHTMLBlock, OpParagraph, OpBlockquoteStart, OpBulletListStart, OpOrderedListStart: // Start of new block that does not coincide with the start of an outer // block. return last != OpBlockquoteStart && last != OpListItemStart case OpListItemStart: // A list item that is not the first in the list. The first item is // already handled when OpBulletListStart or OpOrderedListStart is seen. return last != OpBulletListStart && last != OpOrderedListStart } return false } func codeFences(info string, lines []string) (string, string) { var fenceRune rune var runLens map[int]bool if strings.ContainsRune(info, '`') { fenceRune = '~' runLens = matchLens(lines, tildeRunRegexp) } else { fenceRune = '`' runLens = matchLens(lines, backquoteRunRegexp) } l := 3 for x := range runLens { if l < x+1 { l = x + 1 } } fence := strings.Repeat(string(fenceRune), l) if fenceRune == '~' && strings.HasPrefix(info, "~") { return fence + " " + escapeCodeFenceInfo(info), fence } return fence + escapeCodeFenceInfo(info), fence } func escapeCodeFenceInfo(s string) string { // We could just use escapNewLines(escapeText(info)) here and be correct. // However, some of Elvish website's Markdown files embeds Markdown inside // the info string, and we'd like to leave them as is as much as possible, // so be more conservative in what we escape. // // The info string is mostly verbatim, with only support for backslashes and // entities, so we only need to escape \ and &. Additionally, the info // string can't contain newlines, so escape that too. if !strings.ContainsAny(s, "\\\n&") { return s } var sb strings.Builder for i, r := range s { switch r { case '\\': sb.WriteString(`\\`) case '\n': sb.WriteString(" ") case '&': if leadingCharRef(s[i:]) == "" { sb.WriteByte('&') } else { sb.WriteString(`\&`) } default: sb.WriteRune(r) } } return sb.String() } func allDashBullets(containers []*fmtContainer) bool { for _, ct := range containers { if ct.marker != "- " { return false } } return true } // A segment is a unit of intermediate output when formatting inline content. type segment struct { typ segmentType text string } type segmentType uint const ( segText segmentType = iota // Some texts cannot be reflowed because whitespace is significant. This // includes code spans and link tails. segTextNoReflow segHTML segNewLine segHardLineBreak segLinkOrImageStart segLinkOrImageEnd ) func (c *FmtCodec) buildSegments(ops []InlineOp) []segment { var segs []segment write := func(s string) { if s != "" { segs = append(segs, segment{typ: segText, text: s}) } } writeNoReflow := func(s string) { segs = append(segs, segment{typ: segTextNoReflow, text: s}) } emphasis := 0 for i, op := range ops { switch op.Type { case OpText: text := op.Text if i > 0 && isEmphasisStart(ops[i-1]) { if r, l := utf8.DecodeRuneInString(text); l > 0 && unicode.IsSpace(r) { // Escape space immediately after emphasis start, since a * // before a space cannot open emphasis. write("&#" + strconv.Itoa(int(r)) + ";") text = text[l:] } } else if i > 1 && isEmphasisEnd(ops[i-1]) && emphasisOutputEndsWithPunct(ops[i-2]) { if r, l := utf8.DecodeRuneInString(text); isWord(r, l) { // Escape "other" (word character) immediately after // emphasis end if emphasis content ends with a punctuation. write("&#" + strconv.Itoa(int(r)) + ";") text = text[l:] } } suffix := "" if strings.HasSuffix(text, "!") && i < len(ops)-1 && ops[i+1].Type == OpLinkStart { text = text[:len(text)-1] suffix = `\!` } else if i < len(ops)-1 && isEmphasisEnd(ops[i+1]) { if r, l := utf8.DecodeLastRuneInString(text); l > 0 && unicode.IsSpace(r) { // Escape space immediately before emphasis end, since a * // after a space cannot close emphasis. text = text[:len(text)-l] suffix = "&#" + strconv.Itoa(int(r)) + ";" } } else if i < len(ops)-2 && isEmphasisStart(ops[i+1]) && emphasisOutputStartsWithPunct(ops[i+2]) { if r, l := utf8.DecodeLastRuneInString(text); isWord(r, l) { // Escape "other" (word character) immediately before // emphasis start if the output of the emphasis content will // start with a punctuation. text = text[:len(text)-l] suffix = "&#" + strconv.Itoa(int(r)) + ";" } } write(escapeText(text)) write(suffix) case OpRawHTML: segs = append(segs, segment{typ: segHTML, text: op.Text}) case OpNewLine: if i > 0 && isEmphasisStart(ops[i-1]) || i < len(ops)-1 && isEmphasisEnd(ops[i+1]) { write(" ") } else { segs = append(segs, segment{typ: segNewLine, text: op.Text}) } case OpCodeSpan: text := op.Text hasRunWithLen := matchLens([]string{text}, backquoteRunRegexp) l := 1 for hasRunWithLen[l] { l++ } delim := strings.Repeat("`", l) // Code span text is never empty first := text[0] last := text[len(text)-1] addSpace := first == '`' || last == '`' || (first == ' ' && last == ' ' && strings.Trim(text, " ") != "") var sb strings.Builder sb.WriteString(delim) if addSpace { sb.WriteByte(' ') } sb.WriteString(text) if addSpace { sb.WriteByte(' ') } sb.WriteString(delim) segs = append(segs, segment{typ: segTextNoReflow, text: sb.String()}) case OpEmphasisStart: write("*") emphasis++ if emphasis >= 2 { c.setUnsupported().NestedEmphasisOrStrongEmphasis = true } if i > 0 && isEmphasisEnd(ops[i-1]) { c.setUnsupported().ConsecutiveEmphasisOrStrongEmphasis = true } case OpEmphasisEnd: write("*") emphasis-- case OpStrongEmphasisStart: write("**") emphasis++ if emphasis >= 2 { c.setUnsupported().NestedEmphasisOrStrongEmphasis = true } if i > 0 && isEmphasisEnd(ops[i-1]) { c.setUnsupported().ConsecutiveEmphasisOrStrongEmphasis = true } case OpStrongEmphasisEnd: write("**") emphasis-- case OpLinkStart: segs = append(segs, segment{typ: segLinkOrImageStart}) write("[") case OpLinkEnd: write("]") writeNoReflow(formatLinkTail(op.Dest, op.Text)) segs = append(segs, segment{typ: segLinkOrImageEnd}) case OpImage: segs = append(segs, segment{typ: segLinkOrImageStart}) write("![") write(escapeNewLines(escapeText(op.Alt))) write("]") writeNoReflow(formatLinkTail(op.Dest, op.Text)) segs = append(segs, segment{typ: segLinkOrImageEnd}) case OpAutolink: write("<") if op.Dest == "mailto:"+op.Text { // Don't escape email autolinks. This is because the regexp that // matches email autolinks does not allow ";", so escaping them // makes the output no longer an email autolink. write(op.Text) } else { write(escapeAutolink(op.Text)) } write(">") case OpHardLineBreak: segs = append(segs, segment{typ: segHardLineBreak}) } } return segs } var atxHeadingCloserLookalike = regexp.MustCompile(`#+$`) func (c *FmtCodec) writeSegmentsATXHeading(segs []segment) { for i, seg := range segs { switch seg.typ { case segText: text := seg.text if i == 0 { text = escapeLeadingSpaceTab(text) } if i == len(segs)-1 { text = escapeTrailingSpaceTab(text) if text[len(text)-1] == '#' { if hashes := atxHeadingCloserLookalike.FindString(text); hashes != "" { head := text[:len(text)-len(hashes)] if endsWithSpaceOrTab(head) || (head == "" && i == 0) { text = head + `\` + hashes } } } } c.write(text) case segHTML, segTextNoReflow: // Raw HTML in ATX headings and code spans never contain embedded // newlines, so just write them as is. c.write(seg.text) case segNewLine: c.write(" ") } } } func (c *FmtCodec) writeSegmentsParagraph(segs []segment) { for i := 0; i < len(segs); i++ { seg := segs[i] startOfLine := i == 0 || (segs[i-1].typ == segNewLine && (i-1 == 0 || segs[i-2].typ != segNewLine)) endOfLine := i == len(segs)-1 || segs[i+1].typ == segNewLine switch seg.typ { case segText: text := seg.text // Escape trailing space or tab first. This way text like "- - - " // alone on a line will already have the trailing space escaped and // can no longer be parsed as a thematic break. if endOfLine { text = escapeTrailingSpaceTab(text) } if startOfLine { text = c.escapeStartOfLine(text, i == 0, endOfLine) } c.write(text) case segHTML: // Inline raw HTML may contain embedded newlines; write them // separately. lines := strings.Split(seg.text, "\n") if startOfLine && canStartHTMLBlock(lines[0], i == 0) { // If the first line appears at the start of the line, check // whether it can also be parsed as an HTML block instead. if i > 0 { // If the raw HTML appears not at the start of a paragraph, // inserting 4 spaces will prevent an HTML block to be // parsed, and won't make the text parse as an indented code // block, since the latter can't interrupt a paragraph. c.write(" ") } else if len(lines) == 1 && i+1 < len(segs) && segs[i+1].typ == segNewLine { // If raw HTML does appear at the start of a paragraph, the // only way I have found (actually the fuzz test found) for // the raw HTML to not have get parsed as an HTML block is // when the raw HTML is one line and is followed by an // escaped newline. c.write(lines[0]) c.write(" ") i++ continue } } c.write(lines[0]) for _, line := range lines[1:] { c.finishLine() c.startLine() c.write(line) } case segTextNoReflow: c.write(seg.text) case segNewLine: if i == 0 || i == len(segs)-1 || segs[i-1].typ == segNewLine { c.write(" ") } else { c.finishLine() c.startLine() } case segHardLineBreak: c.write("\\") } } } var ( whitespaceRunRegexp = regexp.MustCompile(`[ \t\n]+`) ) func (c *FmtCodec) writeSegmentsParagraphReflow(segs []segment, maxWidth int) { // Rearrange the segments into spans with the following properties: // // - Each span must be written as a whole, with no changes it its internal // whitespaces. // // - Adjacent spans must be separated by whitespaces. var spans []string var currentSpan strings.Builder finishCurrentSpan := func() { if currentSpan.Len() > 0 { spans = append(spans, currentSpan.String()) currentSpan.Reset() } } linkOrImage := 0 for _, seg := range segs { switch seg.typ { case segText: parts := whitespaceRunRegexp.Split(seg.text, -1) if linkOrImage > 0 { // Coalesce spaces between segments if parts[0] == "" && strings.HasSuffix(currentSpan.String(), " ") { parts = parts[1:] } currentSpan.WriteString(strings.Join(parts, " ")) } else { for i, part := range parts { if i > 0 { finishCurrentSpan() } currentSpan.WriteString(part) } } case segTextNoReflow: currentSpan.WriteString(seg.text) case segHTML: // Replacing a whitespace run in raw HTML with a single space is not // always correct. For example, the following two are different: // // foo // // foo // // But the few uses of raw inline HTML in Elvish's Markdown are // simple enough (for example just a simple "" tag), so we do // the wrong but easy thing here. The result is accepted by the // automated test, whose simplistic whitespace coalescing algorithm // will coalesce whitespaces within raw HTML. // // The correct way to handle this is to preserve newlines inside raw // HTML. But this will make some spans multi-line and complicate the // process to arrange the spans into lines. currentSpan.WriteString( whitespaceRunRegexp.ReplaceAllLiteralString(seg.text, " ")) case segNewLine: if linkOrImage > 0 { if !strings.HasSuffix(currentSpan.String(), " ") { currentSpan.WriteString(" ") } } else { finishCurrentSpan() } case segHardLineBreak: if linkOrImage > 0 { // Keep links and images on the same line. currentSpan.WriteString("
") } else { // A span ending in "\\\n" is handled specifically below. currentSpan.WriteString("\\\n") finishCurrentSpan() } case segLinkOrImageStart: linkOrImage++ case segLinkOrImageEnd: linkOrImage-- } } finishCurrentSpan() if len(spans) == 0 { // If there are no spans left, write an ampersand-escaped newline to // preserve the paragraph. A run of ampersand-escaped whitespaces seems // to be the only way to create an empty paragraph in Markdown in the // first place. c.write(" ") return } for _, ct := range c.containers { maxWidth -= len(ct.marker) } var currentLine strings.Builder currentLineWidth := 0 startOfParagraph := true writeCurrentLine := func() { escaped := c.escapeStartOfLine(currentLine.String(), startOfParagraph, true) if canStartHTMLBlock(escaped, startOfParagraph) { if startOfParagraph { escaped = " " + escaped } else { escaped = " " + escaped } } if !startOfParagraph { c.startLine() } c.write(escaped) } startNewLine := func() { c.finishLine() currentLine.Reset() currentLineWidth = 0 startOfParagraph = false } for i, span := range spans { // Only spans ending in a hard line break ends in a newline hardLineBreak := strings.HasSuffix(span, "\n") if hardLineBreak { span = span[:len(span)-1] } w := wcwidth.Of(span) if currentLine.Len() == 0 { currentLine.WriteString(span) currentLineWidth = w } else { // Determine whether the current span fits onto the current line. // // One slightly tricky detail here is that c.escapeStartOfLine may // insert more text, making the line wider. In reflow mode, the line // never starts or ends with whitespaces, so the most we have to // worry about is one backslash. // // As a result, if the line's width is exactly maxWidth after // appending the current span, we need to be extra careful and only // consider the current span to fit if c.escapeStartOfLine won't // introduce an additional backslash. // // The current implementation of this check is rather inefficient, // but since the check is done at most once per line, the // performance might as well be good enough. fits := false if currentLineWidth+1+w < maxWidth { fits = true } else if currentLineWidth+1+w == maxWidth { line := currentLine.String() + " " + span fits = c.escapeStartOfLine(line, startOfParagraph, true) == line } if fits { currentLine.WriteByte(' ') currentLine.WriteString(span) currentLineWidth += 1 + w } else { writeCurrentLine() startNewLine() currentLine.WriteString(span) currentLineWidth = w } } if hardLineBreak { writeCurrentLine() startNewLine() if i == len(spans)-1 { // \ at the end of a paragraph becomes a literal \ instead of a // hard line break. Fix this with an ampersand-escaped // whitespace, which seems to be the only way to make a // paragraph end with a hard line break in the first place. currentLine.WriteString(" ") } } } if currentLine.Len() > 0 { writeCurrentLine() } } var ( // Pattern for text that can be parsed as thematic break, possibly after // prepending the some bullet markers. // // - We don't need to consider leading spaces, since they will already be // ampersand-escaped. // // - We don't need to consider "*", since it is always backslash-escaped. thematicBreakLookalike = regexp.MustCompile(`^((?:-[ \t]*)+|(?:_[ \t]*)+)$`) // Pattern for dash bullets at the end of the buffer. trailingDashes = regexp.MustCompile(`(?:- *)*$`) // Pattern for text that can be parsed as an ATX heading opener, if followed // by space, tab or end of line. atxHeadingOpenerLookalike = regexp.MustCompile(`^#{1,6}`) // Pattern for text that can be parsed as an ordered list opener, if // followed by space, tab or end of line. orderedListOpenerLookalike = regexp.MustCompile(`^([0-9]{1,9})([.)])`) ) func (c *FmtCodec) escapeStartOfLine(s string, startOfParagraph, endOfLine bool) string { s = escapeLeadingSpaceTab(s) switch s[0] { case '-', '+': tail := s[1:] if startsWithSpaceOrTab(tail) || (tail == "" && startOfParagraph && endOfLine) { return `\` + s } case '>': return `\` + s case '#': if hashes := atxHeadingOpenerLookalike.FindString(s); hashes != "" { tail := s[len(hashes):] if startsWithSpaceOrTab(tail) || (tail == "" && endOfLine) { return `\` + s } } } if strings.HasPrefix(s, "~~~") { return `\` + s } else if m := orderedListOpenerLookalike.FindStringSubmatch(s); m != nil { tail := s[len(m[0]):] if startsWithSpaceOrTab(tail) || (tail == "" && endOfLine) { number, punct := m[1], m[2] if startOfParagraph || strings.TrimLeft(number, "0") == "1" { return number + `\` + punct + tail } } } else if endOfLine && thematicBreakLookalike.MatchString(s) { // If a line contains a single segment, there is a danger for // the text to be parsed as a thematic break. // // After the escaping above, the text cannot start of end with a // space or tab; the thematicBreakLookalikeRegexp match furthers // guarantees that the text starts with either "-" or "_". line := s if startOfParagraph && s[0] == '-' { // If we are the start of a paragraph, we also need to include // bullet markers that can be merged with the text to form a // thematic break. // // This can only happen for "-": "*" in the content is already // backslash-escaped at this point, while "_" is not a possible // bullet list marker. line = trailingDashes.FindString(c.sb.String()) + line } if thematicBreakRegexp.MatchString(line) { return `\` + s } } return s } // Whether an inline raw HTML element can be parsed as the first line of an HTML // block. func canStartHTMLBlock(s string, startOfParagraph bool) bool { return strings.HasPrefix(s, "<") && (html1Regexp.MatchString(s) || html2Regexp.MatchString(s) || html3Regexp.MatchString(s) || html4Regexp.MatchString(s) || html5Regexp.MatchString(s) || html6Regexp.MatchString(s) || html7Regexp.MatchString(s) && startOfParagraph) } func escapeLeadingSpaceTab(s string) string { switch s[0] { case ' ': return " " + s[1:] case '\t': return " " + s[1:] } return s } func escapeTrailingSpaceTab(s string) string { switch s[len(s)-1] { case ' ': return s[:len(s)-1] + " " case '\t': return s[:len(s)-1] + " " } return s } func startsWithSpaceOrTab(s string) bool { return s != "" && (s[0] == ' ' || s[0] == '\t') } func endsWithSpaceOrTab(s string) bool { return s != "" && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') } func emphasisOutputStartsWithPunct(op InlineOp) bool { switch op.Type { case OpText: r, l := utf8.DecodeRuneInString(op.Text) // If the content starts with a space, it will be escaped into " " return l > 0 && unicode.IsSpace(r) || isUnicodePunct(r) default: return true } } func emphasisOutputEndsWithPunct(op InlineOp) bool { switch op.Type { case OpText: r, l := utf8.DecodeLastRuneInString(op.Text) // If the content starts with a space, it will be escaped into " " return l > 0 && unicode.IsSpace(r) || isUnicodePunct(r) default: return true } } func matchLens(pieces []string, pattern *regexp.Regexp) map[int]bool { hasRunWithLen := make(map[int]bool) for _, piece := range pieces { for _, run := range pattern.FindAllString(piece, -1) { hasRunWithLen[len(run)] = true } } return hasRunWithLen } const asciiControl = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" const forbiddenInRawLinkDest = asciiControl + " " func formatLinkTail(dest, title string) string { var sb strings.Builder sb.WriteString("(") if strings.ContainsAny(dest, forbiddenInRawLinkDest) || !balancedParens(dest) { // Angle-bracketed destinations recognize a few characters plus // character references as special and disallow newlines. The order of // function calls is important here to avoid double-escaping. sb.WriteString("<" + strings.ReplaceAll( escapeAmpersandBackslash(dest, "<>"), "\n", " ") + ">") } else if dest == "" && title != "" { sb.WriteString("<>") } else { // Bare destinations only recognize backslash and character references // as special. The order of function calls is important here to avoid // double-escaping. escapedDest := escapeAmpersandBackslash(dest, "") // Also escape any leading < so that it won't be parsed as an // angle-bracketed destination. if strings.HasPrefix(escapedDest, "<") { escapedDest = `\` + escapedDest } sb.WriteString(escapedDest) } if title != "" { sb.WriteString(" ") sb.WriteString(escapeNewLines(wrapAndEscapeLinkTitle(title))) } sb.WriteString(")") return sb.String() } func balancedParens(s string) bool { balance := 0 for i := 0; i < len(s); i++ { switch s[i] { case '(': balance++ case ')': if balance == 0 { return false } balance-- } } return balance == 0 } func wrapAndEscapeLinkTitle(title string) string { doubleQuotes := strings.Count(title, "\"") if doubleQuotes == 0 { return "\"" + escapeAmpersandBackslash(title, "") + "\"" } singleQuotes := strings.Count(title, "'") if singleQuotes == 0 { return "'" + escapeAmpersandBackslash(title, "") + "'" } parens := strings.Count(title, "(") + strings.Count(title, ")") if parens == 0 { return "(" + escapeAmpersandBackslash(title, "") + ")" } switch { case doubleQuotes <= singleQuotes && doubleQuotes <= parens: return `"` + escapeAmpersandBackslash(title, `"`) + `"` case singleQuotes <= parens: return "'" + escapeAmpersandBackslash(title, `'`) + "'" default: return "(" + escapeAmpersandBackslash(title, "()") + ")" } } // Backslash-escape ampersands, backslashes and bytes in the specified set. func escapeAmpersandBackslash(s, set string) string { var sb strings.Builder for i := 0; i < len(s); i++ { if s[i] == '\\' || strings.IndexByte(set, s[i]) >= 0 || leadingCharRef(s[i:]) != "" { sb.WriteByte('\\') } sb.WriteByte(s[i]) } return sb.String() } func (c *FmtCodec) startLine() { startLine(c, c.containers) } func (c *FmtCodec) writeLine(s string) { writeLine(c, c.containers, s) } func (c *FmtCodec) finishLine() { c.write("\n") } func (c *FmtCodec) write(s string) { c.sb.WriteString(s) } type writer interface{ write(string) } func startLine(w writer, containers stack[*fmtContainer]) { for _, container := range containers { w.write(container.useMarker()) } } func writeLine(w writer, containers stack[*fmtContainer], s string) { if s == "" { // When writing a blank line, trim trailing spaces from the markers. // // This duplicates startLine, but merges the markers for ease of // trimming. var markers strings.Builder for _, container := range containers { markers.WriteString(container.useMarker()) } w.write(strings.TrimRight(markers.String(), " ")) w.write("\n") return } startLine(w, containers) w.write(s) w.write("\n") } type fmtContainer struct { typ fmtContainerType punct rune // punctuation used to build the marker number int // only used when typ == fmtOrderedItem marker string // starter or continuation marker } type fmtContainerType uint const ( fmtBlockquote fmtContainerType = iota fmtBulletItem fmtOrderedItem ) func (ct *fmtContainer) useMarker() string { m := ct.marker if ct.typ != fmtBlockquote { ct.marker = strings.Repeat(" ", wcwidth.Of(m)) } return m } func pickPunct(def, alt, banned rune) rune { if def != banned { return def } return alt } func isEmphasisStart(op InlineOp) bool { return op.Type == OpEmphasisStart || op.Type == OpStrongEmphasisStart } func isEmphasisEnd(op InlineOp) bool { return op.Type == OpEmphasisEnd || op.Type == OpStrongEmphasisEnd } func escapeNewLines(s string) string { return strings.ReplaceAll(s, "\n", " ") } func escapeText(s string) string { if !strings.ContainsAny(s, "[]*_`\\&<>\u00A0") { return s } var sb strings.Builder for i, r := range s { switch r { case '[', ']', '*', '`', '\\': sb.WriteByte('\\') sb.WriteRune(r) case '_': if isWord(utf8.DecodeLastRuneInString(s[:i])) && isWord(utf8.DecodeRuneInString(s[i+1:])) { sb.WriteByte('_') } else { sb.WriteString(`\_`) } case '&': // Look ahead decide whether the ampersand can start a character // reference and thus needs to be escaped. Since any inline markup // will introduce a metacharacter that is not allowed within // character reference, it is sufficient to check within the text. if leadingCharRef(s[i:]) == "" { sb.WriteByte('&') } else { sb.WriteString(`\&`) } case '<': if i < len(s)-1 && !canBeSpecialAfterLt(s[i+1]) { sb.WriteByte('<') } else { sb.WriteString(`\<`) } case '\u00A0': // This is by no means required, but it's nice to make non-breaking // spaces explicit. sb.WriteString(" ") default: sb.WriteRune(r) } } return sb.String() } const forbiddenInAutolink = asciiControl + "& <>" // The escape of autolinks need to be handled specifically, because they support // character references, but don't support backslashes. Moreover, characters // forbidden inside autolinks (see uriAutolinkRegexp) should also be escaped. func escapeAutolink(s string) string { if !strings.ContainsAny(s, forbiddenInAutolink) { return s } var sb strings.Builder for i := 0; i < len(s); i++ { if s[i] <= 0x20 { sb.WriteString("&#" + strconv.Itoa(int(s[i])) + ";") } else if s[i] == '&' { if leadingCharRef(s[i:]) == "" { sb.WriteByte('&') } else { sb.WriteString("&") } } else if s[i] == '<' { sb.WriteString("<") } else if s[i] == '>' { sb.WriteString(">") } else { sb.WriteByte(s[i]) } } return sb.String() } // Takes the result of utf8.Decode*, and returns whether the character is // non-empty and a "word" character for the purpose of emphasis parsing. func isWord(r rune, l int) bool { return l > 0 && !unicode.IsSpace(r) && !isUnicodePunct(r) } func canBeSpecialAfterLt(b byte) bool { return /* Can form raw HTML */ b == '!' || b == '?' || b != '/' || isASCIILetter(b) || /* Can form email autolink */ '0' <= b && b <= '9' || strings.IndexByte(emailLocalPuncts, b) >= 0 } elvish-0.21.0/pkg/md/fmt_test.go000066400000000000000000000165211465720375400164430ustar00rootroot00000000000000package md_test import ( "fmt" "html" "regexp" "strings" "testing" "unicode/utf8" "github.com/google/go-cmp/cmp" . "src.elv.sh/pkg/md" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/wcwidth" ) var supplementalFmtCases = []testCase{ { Section: "Fenced code blocks", Name: "Tilde fence with info starting with tilde", Markdown: "~~~ ~`\n" + "~~~", }, { Section: "Emphasis and strong emphasis", Name: "Space at start of content", Markdown: "* x*", }, { Section: "Emphasis and strong emphasis", Name: "Space at end of content", Markdown: "*x *", }, { Section: "Emphasis and strong emphasis", Name: "Emphasis opener after word before punctuation", Markdown: "A*!*", }, { Section: "Emphasis and strong emphasis", Name: "Emphasis closer after punctuation before word", Markdown: "*!*A", }, { Section: "Emphasis and strong emphasis", Name: "Space-only content", Markdown: "* *", }, { Section: "Links", Name: "Exclamation mark before link", Markdown: `\![a](b)`, }, { Section: "Links", Name: "Link title with both single and double quotes", Markdown: `[a](b ('"))`, }, { Section: "Links", Name: "Link title with fewer double quotes than single quotes and parens", Markdown: `[a](b "\"''()")`, }, { Section: "Links", Name: "Link title with fewer single quotes than double quotes and parens", Markdown: `[a](b '\'""()')`, }, { Section: "Links", Name: "Link title with fewer parens than single and double quotes", Markdown: `[a](b (\(''""))`, }, { Section: "Links", Name: "Newline in link destination", Markdown: `[a](< >)`, }, { Section: "Soft line breaks", Name: "Space at start of line", Markdown: " foo", }, { Section: "Soft line breaks", Name: "Space at end of line", Markdown: "foo ", }, } var fmtTestCases = concat(htmlTestCases, supplementalFmtCases) func TestFmtPreservesHTMLRender(t *testing.T) { testutil.Set(t, &UnescapeHTML, html.UnescapeString) for _, tc := range fmtTestCases { t.Run(tc.testName(), func(t *testing.T) { testFmtPreservesHTMLRender(t, tc.Markdown) }) } } func FuzzFmtPreservesHTMLRender(f *testing.F) { for _, tc := range fmtTestCases { f.Add(tc.Markdown) } f.Fuzz(testFmtPreservesHTMLRender) } func testFmtPreservesHTMLRender(t *testing.T, original string) { testFmtPreservesHTMLRenderModulo(t, original, 0, nil) } func TestReflowFmtPreservesHTMLRenderModuleWhitespaces(t *testing.T) { testReflowFmt(t, testReflowFmtPreservesHTMLRenderModuloWhitespaces) } func FuzzReflowFmtPreservesHTMLRenderModuleWhitespaces(f *testing.F) { fuzzReflowFmt(f, testReflowFmtPreservesHTMLRenderModuloWhitespaces) } var ( paragraph = regexp.MustCompile(`(?s)

.*?

`) whitespaceRun = regexp.MustCompile(`[ \t\n]+`) brWithWhitespaces = regexp.MustCompile(`[ \t\n]*
[ \t\n]*`) ) func testReflowFmtPreservesHTMLRenderModuloWhitespaces(t *testing.T, original string, w int) { if strings.Contains(original, "

") { t.Skip("markdown contains

") } if strings.Contains(original, "

") { t.Skip("markdown contains

") } testFmtPreservesHTMLRenderModulo(t, original, w, func(html string) string { // Coalesce whitespaces in each paragraph. return paragraph.ReplaceAllStringFunc(html, func(p string) string { body := strings.Trim(p[3:len(p)-4], " \t\n") // Convert each whitespace run to a single space. body = whitespaceRun.ReplaceAllLiteralString(body, " ") // Remove whitespaces around
. body = brWithWhitespaces.ReplaceAllLiteralString(body, "
") return "

" + body + "

" }) }) } func TestReflowFmtResultIsUnchangedUnderFmt(t *testing.T) { testReflowFmt(t, testReflowFmtResultIsUnchangedUnderFmt) } func FuzzReflowFmtResultIsUnchangedUnderFmt(f *testing.F) { fuzzReflowFmt(f, testReflowFmtResultIsUnchangedUnderFmt) } func testReflowFmtResultIsUnchangedUnderFmt(t *testing.T, original string, w int) { reflowed := formatAndSkipIfUnsupported(t, original, w) formatted := RenderString(reflowed, &FmtCodec{}) if reflowed != formatted { t.Errorf("original:\n%s\nreflowed:\n%s\nformatted:\n%s"+ "markdown diff (-reflowed +formatted):\n%s", hr+"\n"+original+hr, hr+"\n"+reflowed+hr, hr+"\n"+formatted+hr, cmp.Diff(reflowed, formatted)) } } func TestReflowFmtResultFitsInWidth(t *testing.T) { testReflowFmt(t, testReflowFmtResultFitsInWidth) } func FuzzReflowFmtResultFitsInWidth(f *testing.F) { fuzzReflowFmt(f, testReflowFmtResultFitsInWidth) } var ( // Match all markers that can be written by FmtCodec. markersRegexp = regexp.MustCompile(`^ *(?:(?:[-*>]|[0-9]{1,9}[.)]) *)*`) linkRegexp = regexp.MustCompile(`\[.*\]\(.*\)`) codeSpanRegexp = regexp.MustCompile("`.*`") ) func testReflowFmtResultFitsInWidth(t *testing.T, original string, w int) { if w <= 0 { t.Skip("width <= 0") } var trace TraceCodec Render(original, &trace) for _, op := range trace.Ops() { switch op.Type { case OpHeading, OpCodeBlock, OpHTMLBlock: t.Skipf("input contains unsupported block type %s", op.Type) } } reflowed := formatAndSkipIfUnsupported(t, original, w) for _, line := range strings.Split(reflowed, "\n") { lineWidth := wcwidth.Of(line) if lineWidth <= w { continue } // Strip all markers content := line[len(markersRegexp.FindString(line)):] // Analyze whether the content is allowed to exceed width switch { case !strings.Contains(content, " "): case strings.Contains(content, "<"): case linkRegexp.MatchString(content): case codeSpanRegexp.MatchString(content): default: t.Errorf("line length > %d: %q\nfull reflowed:\n%s", w, line, hr+"\n"+reflowed+hr) } } } var widths = []int{20, 51, 80} func testReflowFmt(t *testing.T, test func(*testing.T, string, int)) { for _, tc := range fmtTestCases { for _, w := range widths { t.Run(fmt.Sprintf("%s/Width %d", tc.testName(), w), func(t *testing.T) { test(t, tc.Markdown, w) }) } } } func fuzzReflowFmt(f *testing.F, test func(*testing.T, string, int)) { for _, tc := range fmtTestCases { for _, w := range widths { f.Add(tc.Markdown, w) } } f.Fuzz(test) } func testFmtPreservesHTMLRenderModulo(t *testing.T, original string, w int, processHTML func(string) string) { formatted := formatAndSkipIfUnsupported(t, original, w) originalRender := RenderString(original, &HTMLCodec{}) formattedRender := RenderString(formatted, &HTMLCodec{}) if processHTML != nil { originalRender = processHTML(originalRender) formattedRender = processHTML(formattedRender) } if formattedRender != originalRender { t.Errorf("original:\n%s\nformatted:\n%s\n"+ "markdown diff (-original +formatted):\n%s"+ "HTML diff (-original +formatted):\n%s"+ "ops diff (-original +formatted):\n%s", hr+"\n"+original+hr, hr+"\n"+formatted+hr, cmp.Diff(original, formatted), cmp.Diff(originalRender, formattedRender), cmp.Diff(RenderString(original, &TraceCodec{}), RenderString(formatted, &TraceCodec{}))) } } func formatAndSkipIfUnsupported(t *testing.T, original string, w int) string { if !utf8.ValidString(original) { t.Skipf("input is not valid UTF-8") } if strings.Contains(original, "\t") { t.Skipf("input contains tab") } codec := &FmtCodec{Width: w} formatted := RenderString(original, codec) if u := codec.Unsupported(); u != nil { t.Skipf("input uses unsupported feature: %v", u) } return formatted } elvish-0.21.0/pkg/md/html.go000066400000000000000000000101041465720375400155510ustar00rootroot00000000000000package md import ( "fmt" "strconv" "strings" ) var ( escapeHTML = strings.NewReplacer( "&", "&", `"`, """, "<", "<", ">", ">", // No need to escape single quotes, since attributes in the output // always use double quotes. ).Replace // Modern browsers will happily accept almost anything in a URL attribute, // except for the quote used by the attribute and space. But we try to be // conservative and escape some characters, mostly following // https://url.spec.whatwg.org/#url-code-points. // // We don't bother escaping control characters as they are unlikely to // appear in Markdown text. escapeURL = strings.NewReplacer( `"`, "%22", `\`, "%5C", " ", "%20", "`", "%60", "[", "%5B", "]", "%5D", "<", "%3C", ">", "%3E").Replace ) // HTMLCodec converts markdown to HTML. type HTMLCodec struct { strings.Builder // If non-nil, will be called for each code block. The return value is // inserted into the HTML output and should be properly escaped. ConvertCodeBlock func(info, code string) string } var tags = []string{ OpThematicBreak: "
\n", OpBlockquoteStart: "
\n", OpBlockquoteEnd: "
\n", OpListItemStart: "
  • \n", OpListItemEnd: "
  • \n", OpBulletListStart: "
      \n", OpBulletListEnd: "
    \n", OpOrderedListEnd: "\n", } func (c *HTMLCodec) Do(op Op) { switch op.Type { case OpHeading: var attrs attrBuilder if op.Info != "" { // Only support #id since that's the only thing used in Elvish's // Markdown right now. More can be added if needed. if op.Info[0] == '#' { attrs.set("id", op.Info[1:]) } } fmt.Fprintf(c, "", op.Number, &attrs) RenderInlineContentToHTML(&c.Builder, op.Content) fmt.Fprintf(c, "\n", op.Number) case OpCodeBlock: var attrs attrBuilder language := "" if op.Info != "" { language, _, _ = strings.Cut(op.Info, " ") attrs.set("class", "language-"+language) } fmt.Fprintf(c, "
    ", &attrs)
    		if c.ConvertCodeBlock != nil {
    			c.WriteString(c.ConvertCodeBlock(op.Info, strings.Join(op.Lines, "\n")+"\n"))
    		} else {
    			for _, line := range op.Lines {
    				c.WriteString(escapeHTML(line))
    				c.WriteByte('\n')
    			}
    		}
    		c.WriteString("
    \n") case OpHTMLBlock: for _, line := range op.Lines { c.WriteString(line) c.WriteByte('\n') } case OpParagraph: c.WriteString("

    ") RenderInlineContentToHTML(&c.Builder, op.Content) c.WriteString("

    \n") case OpOrderedListStart: var attrs attrBuilder if op.Number != 1 { attrs.set("start", strconv.Itoa(op.Number)) } fmt.Fprintf(c, "\n", &attrs) default: c.WriteString(tags[op.Type]) } } var inlineTags = []string{ OpNewLine: "\n", OpEmphasisStart: "", OpEmphasisEnd: "", OpStrongEmphasisStart: "", OpStrongEmphasisEnd: "", OpLinkEnd: "", OpHardLineBreak: "
    ", } // RenderInlineContentToHTML renders inline content to HTML, writing to a // [strings.Builder]. This is useful for implementing an alternative // HTML-outputting [Codec]. func RenderInlineContentToHTML(sb *strings.Builder, ops []InlineOp) { for _, op := range ops { doInline(sb, op) } } func doInline(sb *strings.Builder, op InlineOp) { switch op.Type { case OpText: sb.WriteString(escapeHTML(op.Text)) case OpCodeSpan: sb.WriteString("") sb.WriteString(escapeHTML(op.Text)) sb.WriteString("") case OpRawHTML: sb.WriteString(op.Text) case OpLinkStart: var attrs attrBuilder attrs.set("href", escapeURL(op.Dest)) if op.Text != "" { attrs.set("title", op.Text) } fmt.Fprintf(sb, "", &attrs) case OpImage: var attrs attrBuilder attrs.set("src", escapeURL(op.Dest)) attrs.set("alt", op.Alt) if op.Text != "" { attrs.set("title", op.Text) } fmt.Fprintf(sb, "", &attrs) case OpAutolink: var attrs attrBuilder attrs.set("href", escapeURL(op.Dest)) fmt.Fprintf(sb, "%s", &attrs, escapeHTML(op.Text)) default: sb.WriteString(inlineTags[op.Type]) } } type attrBuilder struct{ strings.Builder } func (a *attrBuilder) set(k, v string) { fmt.Fprintf(a, ` %s="%s"`, k, escapeHTML(v)) } elvish-0.21.0/pkg/md/html_test.go000066400000000000000000000030601465720375400166130ustar00rootroot00000000000000package md_test import ( "fmt" "html" "strings" "testing" "github.com/google/go-cmp/cmp" . "src.elv.sh/pkg/md" "src.elv.sh/pkg/testutil" ) // The spec contains some tests where non-ASCII characters get escaped in URLs. var escapeURLAttr = strings.NewReplacer( `"`, "%22", `\`, "%5C", " ", "%20", "`", "%60", "[", "%5B", "]", "%5D", "<", "%3C", ">", "%3E", "ö", "%C3%B6", "ä", "%C3%A4", " ", "%C2%A0").Replace func TestHTML(t *testing.T) { testutil.Set(t, &UnescapeHTML, html.UnescapeString) testutil.Set(t, EscapeURL, escapeURLAttr) for _, tc := range htmlTestCases { t.Run(tc.testName(), func(t *testing.T) { tc.skipIfNotSupported(t) got := RenderString(tc.Markdown, &HTMLCodec{}) // Try to hide the difference between tight and loose lists by // "loosifying" the output. This only works for tight lists whose // items consist of single lines, so more complex cases are still // skipped. want := loosifyLists(tc.HTML) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("input:\n%s\ndiff (-want +got):\n%s", hr+"\n"+tc.Markdown+hr, diff) } }) } } func TestHTML_ConvertCodeBlock(t *testing.T) { f := func(info, code string) string { return fmt.Sprintf("%s (%q)", info, code) } c := HTMLCodec{ConvertCodeBlock: f} markdown := dedent(` ~~~elvish foo bar echo echo ~~~ `) want := dedent(`
    elvish foo bar ("echo\necho\n")
    `) got := RenderString(markdown, &c) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("diff (-want +got):\n%s", diff) } } elvish-0.21.0/pkg/md/inline.go000066400000000000000000000461131465720375400160740ustar00rootroot00000000000000package md import ( "regexp" "strings" "unicode" "unicode/utf8" ) // InlineOp represents an inline operation. type InlineOp struct { Type InlineOpType // OpText, OpCodeSpan, OpRawHTML, OpAutolink: Text content // OpLinkStart, OpLinkEnd, OpImage: title text Text string // OpLinkStart, OpLinkEnd, OpImage, OpAutolink Dest string // ForOpImage Alt string } // InlineOpType enumerates possible types of an InlineOp. type InlineOpType uint const ( // Text elements. Embedded newlines in OpText are turned into OpNewLine, but // OpRawHTML can contain embedded newlines. OpCodeSpan never contains // embedded newlines. OpText InlineOpType = iota OpCodeSpan OpRawHTML OpNewLine // Inline markup elements. OpEmphasisStart OpEmphasisEnd OpStrongEmphasisStart OpStrongEmphasisEnd OpLinkStart OpLinkEnd OpImage OpAutolink OpHardLineBreak ) // String returns the text content of the InlineOp func (op InlineOp) String() string { switch op.Type { case OpText, OpCodeSpan, OpRawHTML, OpAutolink: return op.Text case OpNewLine: return "\n" case OpImage: return op.Alt } return "" } func renderInline(text string) []InlineOp { p := inlineParser{text, 0, makeDelimStack(), buffer{}} p.render() return p.buf.ops() } type inlineParser struct { text string pos int delims delimStack buf buffer } func (p *inlineParser) render() { for p.pos < len(p.text) { b := p.text[p.pos] begin := p.pos p.pos++ parseText := func() { for p.pos < len(p.text) && !isMeta(p.text[p.pos]) { p.pos++ } text := p.text[begin:p.pos] hardLineBreak := false if p.pos < len(p.text) && p.text[p.pos] == '\n' { // https://spec.commonmark.org/0.31.2/#hard-line-break // // The input to renderInline never ends in a newline, so all // newlines are internal ones, thus subject to the hard line // break rules hardLineBreak = strings.HasSuffix(text, " ") text = strings.TrimRight(text, " ") } p.buf.push(textPiece(text)) if hardLineBreak { p.buf.push(piece{main: InlineOp{Type: OpHardLineBreak}}) } } switch b { // The 3 branches below implement the first part of // https://spec.commonmark.org/0.31.2/#an-algorithm-for-parsing-nested-emphasis-and-links. case '[': bufIdx := p.buf.push(textPiece("[")) p.delims.push(&delim{typ: '[', bufIdx: bufIdx}) case '!': if p.pos < len(p.text) && p.text[p.pos] == '[' { p.pos++ bufIdx := p.buf.push(textPiece("![")) p.delims.push(&delim{typ: '!', bufIdx: bufIdx}) } else { parseText() } case '*', '_': p.consumeRun(b) canOpen, canClose := canOpenCloseEmphasis(rune(b), emptyToNewline(utf8.DecodeLastRuneInString(p.text[:begin])), emptyToNewline(utf8.DecodeRuneInString(p.text[p.pos:]))) bufIdx := p.buf.push(textPiece(p.text[begin:p.pos])) p.delims.push( &delim{typ: b, bufIdx: bufIdx, n: p.pos - begin, canOpen: canOpen, canClose: canClose}) case ']': // https://spec.commonmark.org/0.31.2/#look-for-link-or-image. var opener *delim for d := p.delims.top.prev; d != p.delims.bottom; d = d.prev { if d.typ == '[' || d.typ == '!' { opener = d break } } if opener == nil || opener.inactive { if opener != nil { unlink(opener) } p.buf.push(textPiece("]")) continue } n, dest, title := parseLinkTail(p.text[p.pos:]) if n == -1 { unlink(opener) p.buf.push(textPiece("]")) continue } p.pos += n p.processEmphasis(opener) if opener.typ == '[' { for d := opener.prev; d != p.delims.bottom; d = d.prev { if d.typ == '[' { d.inactive = true } } } unlink(opener) if opener.typ == '[' { p.buf.pieces[opener.bufIdx] = piece{ before: []InlineOp{{Type: OpLinkStart, Dest: dest, Text: title}}} p.buf.push(piece{ after: []InlineOp{{Type: OpLinkEnd, Dest: dest, Text: title}}}) } else { // Use the pieces after "![" to build the image alt text. var altBuilder strings.Builder for _, piece := range p.buf.pieces[opener.bufIdx+1:] { altBuilder.WriteString(piece.main.String()) } p.buf.pieces = p.buf.pieces[:opener.bufIdx] alt := altBuilder.String() p.buf.push(piece{ main: InlineOp{Type: OpImage, Dest: dest, Alt: alt, Text: title}}) } case '`': // https://spec.commonmark.org/0.31.2/#code-spans p.consumeRun('`') closer := findBacktickRun(p.text, p.text[begin:p.pos], p.pos) if closer == -1 { // No matching closer, don't parse as code span. parseText() continue } p.buf.push(piece{ main: InlineOp{Type: OpCodeSpan, Text: normalizeCodeSpanContent(p.text[p.pos:closer])}}) p.pos = closer + (p.pos - begin) case '<': // https://spec.commonmark.org/0.31.2/#raw-html if p.pos == len(p.text) { parseText() continue } parseWithRegexp := func(pattern *regexp.Regexp) bool { html := pattern.FindString(p.text[begin:]) if html == "" { return false } p.buf.push(htmlPiece(html)) p.pos = begin + len(html) return true } parseWithCloser := func(closer string) bool { i := strings.Index(p.text[p.pos:], closer) if i == -1 { return false } p.pos += i + len(closer) p.buf.push(htmlPiece(p.text[begin:p.pos])) return true } switch p.text[p.pos] { case '!': switch { case strings.HasPrefix(p.text[p.pos:], "!--"): // Try parsing a comment. if parseWithCloser("-->") { continue } case strings.HasPrefix(p.text[p.pos:], "![CDATA["): // Try parsing a CDATA section if parseWithCloser("]]>") { continue } case p.pos+1 < len(p.text) && isASCIILetter(p.text[p.pos+1]): // Try parsing a declaration. if parseWithCloser(">") { continue } } case '?': // Try parsing a processing instruction. closer := strings.Index(p.text[p.pos:], "?>") if closer != -1 { p.buf.push(htmlPiece(p.text[begin : p.pos+closer+2])) p.pos += closer + 2 continue } case '/': // Try parsing a closing tag. if parseWithRegexp(closingTagRegexp) { continue } default: // Try parsing a open tag. if parseWithRegexp(openTagRegexp) { continue } else { // Try parsing an autolink. autolink := uriAutolinkRegexp.FindString(p.text[begin:]) email := false if autolink == "" { autolink = emailAutolinkRegexp.FindString(p.text[begin:]) email = true } if autolink != "" { p.pos = begin + len(autolink) // Autolinks support character references but not // backslashes, so UnescapeHTML gives us the desired // behavior. text := UnescapeHTML(autolink[1 : len(autolink)-1]) dest := text if email { dest = "mailto:" + dest } p.buf.push(piece{ main: InlineOp{Type: OpAutolink, Text: text, Dest: dest}, }) continue } } } parseText() case '&': // https://spec.commonmark.org/0.31.2/#entity-and-numeric-character-references if entity := leadingCharRef(p.text[begin:]); entity != "" { p.buf.push(textPiece(UnescapeHTML(entity))) p.pos = begin + len(entity) } else { parseText() } case '\\': // https://spec.commonmark.org/0.31.2/#backslash-escapes if p.pos < len(p.text) { if p.text[p.pos] == '\n' { // https://spec.commonmark.org/0.31.2/#hard-line-break // // Do *not* consume the newline; "\\\n" is a hard line break // plus a (soft) line break. p.buf.push(piece{main: InlineOp{Type: OpHardLineBreak}}) continue } else if isASCIIPunct(p.text[p.pos]) { // Valid backslash escape: handle this by just discarding // the backslash. The parseText call below will consider the // next byte to be already included in the text content. begin++ p.pos++ } } parseText() case '\n': // Hard line breaks are already inserted using lookahead in // parseText and the case '\\' branch. p.buf.push(piece{main: InlineOp{Type: OpNewLine}}) // Remove spaces at the beginning of the next line per // https://spec.commonmark.org/0.31.2/#soft-line-breaks. for p.pos < len(p.text) && p.text[p.pos] == ' ' { p.pos++ } default: parseText() } } p.processEmphasis(p.delims.bottom) } func (p *inlineParser) consumeRun(b byte) { for p.pos < len(p.text) && p.text[p.pos] == b { p.pos++ } } // Processes the (rune, int) result of utf8.Decode* so that an empty result is // converted to '\n'. func emptyToNewline(r rune, l int) rune { if l == 0 { return '\n' } return r } // Returns whether an emphasis punctuation can open or close an emphasis, when // following prev and preceding next. Start and end of file should be // represented by '\n'. // // The criteria are described in: // https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis // // The algorithm is a bit complicated. Here is another way to describe the // criteria: // // - Every rune falls into one of three categories: space, punctuation and // other. "Other" is the category of word runes in "intraword emphasis". // // - The following tables describe whether a punctuation can open or close // emphasis: // // Can open emphasis: // // | | next space | next punct | next other | // | ---------- | ---------- | ---------- | ---------- | // | prev space | | _ or * | _ or * | // | prev punct | | _ or * | _ or * | // | prev other | | | only * | // // Can close emphasis: // // | | next space | next punct | next other | // | ---------- | ---------- | ---------- | ---------- | // | prev space | | | | // | prev punct | _ or * | _ or * | | // | prev other | _ or * | _ or * | only * | func canOpenCloseEmphasis(b, prev, next rune) (bool, bool) { leftFlanking := !unicode.IsSpace(next) && (!isUnicodePunct(next) || unicode.IsSpace(prev) || isUnicodePunct(prev)) rightFlanking := !unicode.IsSpace(prev) && (!isUnicodePunct(prev) || unicode.IsSpace(next) || isUnicodePunct(next)) if b == '*' { return leftFlanking, rightFlanking } return leftFlanking && (!rightFlanking || isUnicodePunct(prev)), rightFlanking && (!leftFlanking || isUnicodePunct(next)) } // Returns the starting index of the next backtick run identical to the given // run, starting from i. Returns -1 if no such run exists. func findBacktickRun(s, run string, i int) int { for i < len(s) { j := strings.Index(s[i:], run) if j == -1 { return -1 } j += i if j+len(run) == len(s) || s[j+len(run)] != '`' { return j } // Too many backticks; skip over the entire run. for j += len(run); j < len(s) && s[j] == '`'; j++ { } i = j } return -1 } func normalizeCodeSpanContent(s string) string { s = strings.ReplaceAll(s, "\n", " ") if len(s) > 1 && s[0] == ' ' && s[len(s)-1] == ' ' && strings.Trim(s, " ") != "" { return s[1 : len(s)-1] } return s } // https://spec.commonmark.org/0.31.2/#process-emphasis func (p *inlineParser) processEmphasis(bottom *delim) { var openersBottom [2][3][2]*delim for closer := bottom.next; closer != nil; { if !closer.canClose { closer = closer.next continue } openerBottom := &openersBottom[b2i(closer.typ == '_')][closer.n%3][b2i(closer.canOpen)] if *openerBottom == nil { *openerBottom = bottom } var opener *delim for p := closer.prev; p != *openerBottom && p != bottom; p = p.prev { if p.canOpen && p.typ == closer.typ && ((!p.canClose && !closer.canOpen) || (p.n+closer.n)%3 != 0 || (p.n%3 == 0 && closer.n%3 == 0)) { opener = p break } } if opener == nil { *openerBottom = closer.prev if !closer.canOpen { closer.prev.next = closer.next closer.next.prev = closer.prev } closer = closer.next continue } openerPiece := &p.buf.pieces[opener.bufIdx] closerPiece := &p.buf.pieces[closer.bufIdx] strong := len(openerPiece.main.Text) >= 2 && len(closerPiece.main.Text) >= 2 if strong { openerPiece.main.Text = openerPiece.main.Text[2:] openerPiece.append(InlineOp{Type: OpStrongEmphasisStart}) closerPiece.main.Text = closerPiece.main.Text[2:] closerPiece.prepend(InlineOp{Type: OpStrongEmphasisEnd}) } else { openerPiece.main.Text = openerPiece.main.Text[1:] openerPiece.append(InlineOp{Type: OpEmphasisStart}) closerPiece.main.Text = closerPiece.main.Text[1:] closerPiece.prepend(InlineOp{Type: OpEmphasisEnd}) } opener.next = closer closer.prev = opener if openerPiece.main.Text == "" { opener.prev.next = opener.next opener.next.prev = opener.prev } if closerPiece.main.Text == "" { closer.prev.next = closer.next closer.next.prev = closer.prev closer = closer.next } } bottom.next = p.delims.top p.delims.top.prev = bottom } func b2i(b bool) int { if b { return 1 } else { return 0 } } // Stores output of inline rendering. type buffer struct { pieces []piece } func (b *buffer) push(p piece) int { b.pieces = append(b.pieces, p) return len(b.pieces) - 1 } func (b *buffer) ops() []InlineOp { var ops []InlineOp for _, p := range b.pieces { p.iterate(func(op InlineOp) { if op.Type == OpText { // Convert any embedded newlines into OpNewLine, and merge // adjacent OpText's or OpRawHTML's. if op.Text == "" { return } lines := strings.Split(op.Text, "\n") if len(ops) > 0 && ops[len(ops)-1].Type == op.Type { ops[len(ops)-1].Text += lines[0] } else if lines[0] != "" { ops = append(ops, InlineOp{Type: op.Type, Text: lines[0]}) } for _, line := range lines[1:] { ops = append(ops, InlineOp{Type: OpNewLine}) if line != "" { ops = append(ops, InlineOp{Type: op.Type, Text: line}) } } } else { ops = append(ops, op) } }) } return ops } // The algorithm described in // https://spec.commonmark.org/0.31.2/#phase-2-inline-structure involves inserting // nodes before and after existing nodes in the output. The most natural choice // is a doubly linked list; but for simplicity, we use a slice for output nodes, // keep track of nodes that need to be prepended or appended to each node. // // TODO: Compare the performance of this data structure with doubly linked // lists. type piece struct { before []InlineOp main InlineOp after []InlineOp } func textPiece(text string) piece { return piece{main: InlineOp{Type: OpText, Text: text}} } func htmlPiece(html string) piece { return piece{main: InlineOp{Type: OpRawHTML, Text: html}} } func (p *piece) prepend(op InlineOp) { p.before = append(p.before, op) } func (p *piece) append(op InlineOp) { p.after = append(p.after, op) } func (p *piece) iterate(f func(InlineOp)) { for _, op := range p.before { f(op) } f(p.main) for i := len(p.after) - 1; i >= 0; i-- { f(p.after[i]) } } // A delimiter "stack" (actually a doubly linked list), with sentinels as bottom // and top, with the bottom being the head of the list. // // https://spec.commonmark.org/0.31.2/#delimiter-stack type delimStack struct { bottom, top *delim } func makeDelimStack() delimStack { bottom := &delim{} top := &delim{prev: bottom} bottom.next = top return delimStack{bottom, top} } func (s *delimStack) push(n *delim) { n.prev = s.top.prev n.next = s.top s.top.prev.next = n s.top.prev = n } // A node in the delimiter "stack". type delim struct { typ byte bufIdx int prev *delim next *delim // Only used when typ is '[' inactive bool // Only used when typ is '_' or '*'. n int canOpen bool canClose bool } func unlink(n *delim) { n.next.prev = n.prev n.prev.next = n.next } type linkTailParser struct { text string pos int } // Parses the link "tail", the part after the ] that closes the link text. func parseLinkTail(text string) (n int, dest, title string) { p := linkTailParser{text, 0} return p.parse() } // https://spec.commonmark.org/0.31.2/#links func (p *linkTailParser) parse() (n int, dest, title string) { if len(p.text) < 2 || p.text[0] != '(' { return -1, "", "" } p.pos = 1 p.skipWhitespaces() if p.pos == len(p.text) { return -1, "", "" } // Parse an optional link destination. var destBuilder strings.Builder if p.text[p.pos] == '<' { p.pos++ closed := false angleDest: for p.pos < len(p.text) { switch p.text[p.pos] { case '>': p.pos++ closed = true break angleDest case '\n', '<': return -1, "", "" case '\\': destBuilder.WriteByte(p.parseBackslash()) case '&': destBuilder.WriteString(p.parseCharRef()) default: destBuilder.WriteByte(p.text[p.pos]) p.pos++ } } if !closed { return -1, "", "" } } else { parenBalance := 0 bareDest: for p.pos < len(p.text) { if isASCIIControl(p.text[p.pos]) || p.text[p.pos] == ' ' { break } switch p.text[p.pos] { case '(': parenBalance++ destBuilder.WriteByte('(') p.pos++ case ')': if parenBalance == 0 { break bareDest } parenBalance-- destBuilder.WriteByte(')') p.pos++ case '\\': destBuilder.WriteByte(p.parseBackslash()) case '&': destBuilder.WriteString(p.parseCharRef()) default: destBuilder.WriteByte(p.text[p.pos]) p.pos++ } } if parenBalance != 0 { return -1, "", "" } } p.skipWhitespaces() var titleBuilder strings.Builder if p.pos < len(p.text) && strings.ContainsRune("'\"(", rune(p.text[p.pos])) { opener := p.text[p.pos] closer := p.text[p.pos] if closer == '(' { closer = ')' } p.pos++ title: for p.pos < len(p.text) { switch p.text[p.pos] { case closer: p.pos++ break title case opener: // Titles started with "(" does not allow unescaped "(": // https://spec.commonmark.org/0.31.2/#link-title return -1, "", "" case '\\': titleBuilder.WriteByte(p.parseBackslash()) case '&': titleBuilder.WriteString(p.parseCharRef()) default: titleBuilder.WriteByte(p.text[p.pos]) p.pos++ } } } p.skipWhitespaces() if p.pos == len(p.text) || p.text[p.pos] != ')' { return -1, "", "" } return p.pos + 1, destBuilder.String(), titleBuilder.String() } func (p *linkTailParser) skipWhitespaces() { for p.pos < len(p.text) && isWhitespace(p.text[p.pos]) { p.pos++ } } func isWhitespace(b byte) bool { return b == ' ' || b == '\t' || b == '\n' } func (p *linkTailParser) parseBackslash() byte { if p.pos+1 < len(p.text) && isASCIIPunct(p.text[p.pos+1]) { b := p.text[p.pos+1] p.pos += 2 return b } p.pos++ return '\\' } func (p *linkTailParser) parseCharRef() string { if entity := leadingCharRef(p.text[p.pos:]); entity != "" { p.pos += len(entity) return UnescapeHTML(entity) } p.pos++ return p.text[p.pos-1 : p.pos] } func isASCIILetter(b byte) bool { return ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') } func isASCIIControl(b byte) bool { return b < 0x20 } const asciiPuncts = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" func isASCIIPunct(b byte) bool { return strings.IndexByte(asciiPuncts, b) >= 0 } // The CommonMark spec's definition of Unicode punctuation includes both P and S // categories: https://spec.commonmark.org/0.31.2/#unicode-punctuation-character func isUnicodePunct(r rune) bool { return unicode.IsPunct(r) || unicode.IsSymbol(r) } const metas = "![]*_`\\&<\n" func isMeta(b byte) bool { return strings.IndexByte(metas, b) >= 0 } elvish-0.21.0/pkg/md/md.go000066400000000000000000001030351465720375400152130ustar00rootroot00000000000000// Package md implements a Markdown parser. // // To use this package, call [Render] with one of the [Codec] implementations: // // - [HTMLCodec] converts Markdown to HTML. This is used in // [src.elv.sh/website/cmd/md2html], part of Elvish's website toolchain. // // - [FmtCodec] formats Markdown. This is used in [src.elv.sh/cmd/elvmdfmt], // used for formatting Markdown files in the Elvish repo. // // - [TTYCodec] renders Markdown in the terminal. This will be used in a help // system that can used directly from Elvish to render documentation of // Elvish modules. // // # Why another Markdown implementation? // // The Elvish project uses Markdown in the documentation ("[elvdoc]") for the // functions and variables defined in builtin modules. These docs are then // converted to HTML as part of the website; for example, you can read the docs // for builtin functions and variables at https://elv.sh/ref/builtin.html. // // We used to use [Pandoc] to convert the docs from their Markdown sources to // HTML. However, we would also like to expand the elvdoc system in two ways: // // - We would like to support elvdocs in user-defined modules, not just // builtin modules. // // - We would like to allow users to read elvdocs directly from the Elvish // program, in the terminal, without needing a browser or an Internet // connection. // // With these requirements, Elvish itself needs to know how to parse Markdown // sources and render them in the terminal, so we need a Go implementation // instead. There is a good Go implementation, [github.com/yuin/goldmark], but // it is quite large: linking it into Elvish will increase the binary size by // more than 1MB. (There is another popular Markdown implementation, // [github.com/russross/blackfriday/v2], but it doesn't support CommonMark.) // // By having a more narrow focus, this package is much smaller than goldmark, // and can be easily optimized for Elvish's use cases. In contrast to goldmark's // 1MB, including [Render] and [HTMLCodec] in Elvish only increases the binary // size by 150KB. That said, the functionalities provided by this package still // try to be as general as possible, and can potentially be used by other people // interested in a small Markdown implementation. // // Besides elvdocs, Pandoc was also used to convert all the other content on the // Elvish website (https://elv.sh) to HTML. Additionally, [Prettier] used to be // used to format all the Markdown files in the repo. Now that Elvish has its // own Markdown implementation, we can use it not just for rendering elvdocs in // the terminal, but also replace the use of Pandoc and Prettier. These external // tools are decent, but using them still came with some frictions: // // - Even though both are relatively easy to set up, they can still be a // hindrance to casual contributors. // // - Since behavior of these tools can change with version, we explicit // specify their versions in both CI configurations and [contributing // instructions]. But this creates another problem: every time these tools // release new versions, we have to manually bump the versions, and every // contributor also needs to manually update them in their development // environments. // // Replacing external tools with this package removes these frictions. // // Additionally, this package is very easy to extend and optimize to suit // Elvish's needs: // // - We used to custom Pandoc using a mix of shell scripts, templates and Lua // scripts. While these customization options of Pandoc are well documented, // they are not something people are likely to be familiar with. // // With this implementation, everything is now done with Go code. // // - The Markdown formatter is much faster than Prettier, so it's now feasible // to run the formatter every time when saving a Markdown file. // // # Which Markdown variant does this package implement? // // This package implements a large subset of the [CommonMark] spec, with the // following omissions: // // - "\r" and "\r\n" are not supported as line endings. This can be easily // worked around by converting them to "\n" first. // // - Tabs are not supported for defining block structures; use spaces instead. // Tabs in other context are supported. // // - Among HTML entities, only a few are supported: < > "e; ' // &. This is because the full list of HTML entities is very large and // will inflate the binary size. // // If full support for HTML entities are desirable, this can be done by // overriding the [UnescapeHTML] variable with [html.UnescapeString]. // // (Numeric character references like and are fully supported.) // // - [Setext headings] are not supported; use [ATX headings] instead. // // - [Reference links] are not supported; use [inline links] instead. // // - Lists are always considered [loose]. // // The package also supports the following extensions: // // - ATX headers may be followed by [Pandoc header attributes] {...}. // // These omitted features are never used in Elvish's Markdown sources. // // All implemented features pass their relevant CommonMark spec tests, currently // targeting [CommonMark 0.31.2]. See [testutils_test.go] for a complete list of // which spec tests are skipped. // // # Is this package useful outside Elvish? // // Yes! Well, hopefully. Assuming you don't use the features this package omits, // it can be useful in at least the following ways: // // - The implementation is quite lightweight, so you can use it instead of a // more full-features Markdown library if small binary size is important. // // As shown above, the increase in binary size when using this package in // Elvish is about 150KB, compared to more than 1MB when using // [github.com/yuin/goldmark]. You mileage may vary though, since the binary // size increase depends on which packages the binary is already including. // // - The formatter implemented by [FmtCodec] is heavily fuzz-tested to ensure // that it does not alter the semantics of the Markdown. // // Markdown formatting is fraught with tricky edge cases. For example, if a // formatter standardizes all bullet markers to "-", it might reformat "* // --" to "- ---", but the latter will now be parsed as a thematic break. // // Thanks to Go's builtin [fuzzing support], the formatter is able to handle // many such corner cases (at least [all the corner cases found by the // fuzzer]; take a look and try them on other formatters!). There are two // areas - namely nested and consecutive emphasis or strong emphasis - that // are just too tricky to get 100% right that the formatter is not // guaranteed to be correct; the fuzz test explicitly skips those cases. // // Nonetheless, if you are writing a Markdown formatter and care about // correctness, the corner cases will be interesting, regardless of which // language you are using to implement the formatter. // // [Pandoc header attributes]: https://pandoc.org/MANUAL.html#extension-header_attributes // [all the corner cases found by the fuzzer]: https://github.com/elves/elvish/tree/master/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRender // [fuzzing support]: https://go.dev/security/fuzz/ // [loose]: https://spec.commonmark.org/0.31.2/#loose // [Setext headings]: https://spec.commonmark.org/0.31.2/#setext-headings // [ATX headings]: https://spec.commonmark.org/0.31.2/#atx-headings // [testutils_test.go]: https://github.com/elves/elvish/blob/master/pkg/md/testutils_test.go // [elvdoc]: https://github.com/elves/elvish/blob/master/CONTRIBUTING.md#reference-docs // [Pandoc]: https://pandoc.org // [Prettier]: https://prettier.io // [CommonMark]: https://spec.commonmark.org // [contributing instructions]: https://github.com/elves/elvish/blob/master/CONTRIBUTING.md // [inline links]: https://spec.commonmark.org/0.31.2/#inline-link // [Reference links]: https://spec.commonmark.org/0.31.2/#reference-link // [CommonMark 0.31.2]: https://spec.commonmark.org/0.31.2/ package md //go:generate stringer -type=OpType,InlineOpType -output=zstring.go import ( "fmt" "regexp" "strconv" "strings" "sync" ) // UnescapeHTML is used by the parser to unescape HTML entities and numeric // character references. // // The default implementation supports numeric character references, plus a // minimal set of entities that are necessary for writing valid HTML or can // appear in the output of FmtCodec. It can be set to html.UnescapeString for // better CommonMark compliance. var UnescapeHTML = unescapeHTML // https://spec.commonmark.org/0.31.2/#entity-and-numeric-character-references const charRefPattern = `&(?:[a-zA-Z0-9]+|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});` var charRefRegexp = regexp.MustCompile(charRefPattern) var entities = map[string]rune{ // Necessary for writing valid HTML "lt": '<', "gt": '>', "quote": '"', "apos": '\'', "amp": '&', // Not strictly necessary, but could be output by FmtCodec for slightly // nicer text "Tab": '\t', "NewLine": '\n', "nbsp": '\u00A0', } func unescapeHTML(s string) string { return charRefRegexp.ReplaceAllStringFunc(s, func(entity string) string { body := entity[1 : len(entity)-1] if r, ok := entities[body]; ok { return string(r) } else if body[0] == '#' { if body[1] == 'x' || body[1] == 'X' { if num, err := strconv.ParseInt(body[2:], 16, 32); err == nil { return string(rune(num)) } } else { if num, err := strconv.ParseInt(body[1:], 10, 32); err == nil { return string(rune(num)) } } } return entity }) } // Codec is used to render output. type Codec interface { Do(Op) } // Op represents an operation for the Codec. type Op struct { Type OpType // 1-based line number. If the Op spans multiple lines, this identifies the // first line. For the *End types, this identifies the first line that // causes the block to be terminated, which can be the first line of another // block. LineNo int // For OpOrderedListStart (the start number) or OpHeading (as the heading // level) Number int // For OpHeading (attributes inside { }) and OpCodeBlock (text after opening // fence) Info string // For OpCodeBlock and OpHTMLBlock Lines []string // For OpParagraph and OpHeading Content []InlineOp } // OpType enumerates possible types of an Op. type OpType uint // Possible output operations. const ( // Leaf blocks. OpThematicBreak OpType = iota OpHeading OpCodeBlock OpHTMLBlock OpParagraph // Container blocks. OpBlockquoteStart OpBlockquoteEnd OpListItemStart OpListItemEnd OpBulletListStart OpBulletListEnd OpOrderedListStart OpOrderedListEnd ) var initRegexpsOnce sync.Once // Render parses markdown and renders it with a [Codec]. func Render(text string, codec Codec) { // Compiled regular expressions live on the heap. Compiling them lazily // saves memory if this function is never called. initRegexpsOnce.Do(initRegexps) p := blockParser{lines: lineSplitter{text, 0, 0}, codec: codec} p.render() } // StringerCodec is a [Codec] that also implements the String method. type StringerCodec interface { Codec String() string } // Render calls Render(text, codec) and returns codec.String(). This can be a // bit more convenient to use than [Render]. func RenderString(text string, codec StringerCodec) string { Render(text, codec) return codec.String() } type blockParser struct { lines lineSplitter codec Codec tree blockTree } // Block regexps. var thematicBreakRegexp, atxHeadingRegexp, atxHeadingCloserRegexp, atxHeadingAttributeRegexp, codeFenceRegexp, codeFenceCloserRegexp, html1Regexp, html1CloserRegexp, html2Regexp, html2CloserRegexp, html3Regexp, html3CloserRegexp, html4Regexp, html4CloserRegexp, html5Regexp, html5CloserRegexp, html6Regexp, html7Regexp *regexp.Regexp // Inline regexps. var uriAutolinkRegexp, emailAutolinkRegexp, openTagRegexp, closingTagRegexp *regexp.Regexp // Building blocks for regexps. const ( scheme = `[a-zA-Z][a-zA-Z0-9+.-]{1,31}` emailLocalPuncts = ".!#$%&'*+/=?^_`{|}~-" // https://spec.commonmark.org/0.31.2/#open-tag openTag = `<` + `[a-zA-Z][a-zA-Z0-9-]*` + // tag name (`(?:` + `[ \t\n]+` + // whitespace `[a-zA-Z_:][a-zA-Z0-9_\.:-]*` + // attribute name `(?:[ \t\n]*=[ \t\n]*(?:[^ \t\n"'=<>` + "`" + `]+|'[^']*'|"[^"]*"))?` + // attribute value specification `)*`) + // zero or more attributes `[ \t\n]*` + // whitespace `/?>` // https://spec.commonmark.org/0.31.2/#closing-tag closingTag = `` ) func initRegexps() { thematicBreakRegexp = regexp.MustCompile( `^ {0,3}((?:-[ \t]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})$`) // Capture group 1: heading opener atxHeadingRegexp = regexp.MustCompile(`^ {0,3}(#{1,6})(?:[ \t]|$)`) atxHeadingCloserRegexp = regexp.MustCompile(`[ \t]#+[ \t]*$`) // Support the header_attributes extension // (https://pandoc.org/MANUAL.html#extension-header_attributes). Like // pandoc, attributes appear *after* the optional heading closer. // // Attributes are stored in the info string and interpreted by the Codec. atxHeadingAttributeRegexp = regexp.MustCompile(` {([^}]+)}$`) // Capture groups: // 1. Indent // 2. Fence punctuations (backquote fence) // 3. Untrimmed info string (backquote fence) // 4. Fence punctuations (tilde fence) // 5. Untrimmed info string (tilde fence) codeFenceRegexp = regexp.MustCompile("(^ {0,3})(?:(`{3,})([^`]*)|(~{3,})(.*))$") // Capture group 1: fence punctuations codeFenceCloserRegexp = regexp.MustCompile("(?:^ {0,3})(`{3,}|~{3,})[ \t]*$") // These corresponds to the bullet list in // https://spec.commonmark.org/0.31.2/#html-blocks. html1Regexp = regexp.MustCompile(`^ {0,3}<(?i:pre|script|style|textarea)`) html1CloserRegexp = regexp.MustCompile(``) html3Regexp = regexp.MustCompile(`^ {0,3}<\?`) html3CloserRegexp = regexp.MustCompile(`\?>`) html4Regexp = regexp.MustCompile(`^ {0,3}`) html5Regexp = regexp.MustCompile(`^ {0,3}`) html6Regexp = regexp.MustCompile(`^ {0,3}]|$|/>)`) html7Regexp = regexp.MustCompile( fmt.Sprintf(`^ {0,3}(?:%s|%s)[ \t]*$`, openTag, closingTag)) // https://spec.commonmark.org/0.31.2/#uri-autolink uriAutolinkRegexp = regexp.MustCompile( `^<` + scheme + `:[^\x00-\x19 <>]*` + `>`) // https://spec.commonmark.org/0.31.2/#email-autolink emailAutolinkRegexp = regexp.MustCompile( `^<[a-zA-Z0-9` + emailLocalPuncts + `]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>`) openTagRegexp = regexp.MustCompile(`^` + openTag) closingTagRegexp = regexp.MustCompile(`^` + closingTag) } const indentedCodePrefix = " " func (p *blockParser) render() { for p.lines.more() { line, lineNo := p.lines.next() line, matchedContainers, newItem := p.tree.processContainerMarkers(line, lineNo, p.codec) if isBlankLine(line) { // Blank lines terminate blockquote if the continuation marker is // absent. if i, unmatched := p.tree.unmatchedBlockquote(matchedContainers); unmatched { p.tree.closeBlocks(i, lineNo, p.codec) continue } if newItem && p.lines.more() { // A list item can start with at most one blank line; the second // blank closes it. nextLine, _ := p.lines.next() nextLine, _ = p.tree.matchContinuationMarkers(nextLine) p.lines.backup() if isBlankLine(nextLine) { p.tree.closeBlocks(len(p.tree.containers)-1, lineNo, p.codec) } } p.tree.closeParagraph(lineNo, p.codec) } else if thematicBreakRegexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.codec.Do(Op{Type: OpThematicBreak, LineNo: lineNo}) } else if m := atxHeadingRegexp.FindStringSubmatchIndex(line); m != nil { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) openerStart, openerEnd := m[2], m[3] opener := line[openerStart:openerEnd] line = strings.TrimRight(line[openerEnd:], " \t") if closer := atxHeadingCloserRegexp.FindString(line); closer != "" { line = strings.TrimRight(line[:len(line)-len(closer)], " \t") } attr := "" if m := atxHeadingAttributeRegexp.FindStringSubmatch(line); m != nil { attr = m[1] line = strings.TrimRight(line[:len(line)-len(m[0])], " \t") } level := len(opener) p.codec.Do(Op{ Type: OpHeading, LineNo: lineNo, Number: level, Info: attr, Content: renderInline(strings.Trim(line, " \t"))}) } else if m := codeFenceRegexp.FindStringSubmatch(line); m != nil { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) indent, opener, info := len(m[1]), m[2], m[3] if opener == "" { opener, info = m[4], m[5] } p.parseFencedCodeBlock(indent, opener, info) } else if len(p.tree.paragraph) == 0 && strings.HasPrefix(line, indentedCodePrefix) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseIndentedCodeBlock(line) } else if html1Regexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseCloserTerminatedHTMLBlock(line, html1CloserRegexp.MatchString) } else if html2Regexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseCloserTerminatedHTMLBlock(line, html2CloserRegexp.MatchString) } else if html3Regexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseCloserTerminatedHTMLBlock(line, html3CloserRegexp.MatchString) } else if html4Regexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseCloserTerminatedHTMLBlock(line, html4CloserRegexp.MatchString) } else if html5Regexp.MatchString(line) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseCloserTerminatedHTMLBlock(line, html5CloserRegexp.MatchString) } else if html6Regexp.MatchString(line) || (len(p.tree.paragraph) == 0 && html7Regexp.MatchString(line)) { p.tree.closeBlocks(matchedContainers, lineNo, p.codec) p.parseBlankLineTerminatedHTMLBlock(line) } else { if len(p.tree.paragraph) == 0 { // This is not lazy continuation, so close all unmatched // containers. p.tree.closeBlocks(matchedContainers, lineNo, p.codec) } p.tree.paragraph = append(p.tree.paragraph, line) } } p.tree.closeBlocks(0, p.lines.lastLineNo+1, p.codec) } func isBlankLine(line string) bool { return strings.Trim(line, " \t") == "" } func (p *blockParser) parseFencedCodeBlock(indent int, opener, info string) { // Escaped spaces and tabs (e.g. ) should also be trimmed, so process // the info string before trimming. info = strings.Trim(processCodeFenceInfo(info), " \t") var lines []string startLineNo := p.lines.lastLineNo doCodeBlock := func() { p.codec.Do(Op{Type: OpCodeBlock, LineNo: startLineNo, Info: info, Lines: lines}) } for p.lines.more() { line, lineNo := p.lines.next() line, matchedContainers := p.tree.matchContinuationMarkers(line) if isBlankLine(line) { if i, unmatched := p.tree.unmatchedBlockquote(matchedContainers); unmatched { doCodeBlock() p.tree.closeBlocks(i, lineNo, p.codec) return } } else if matchedContainers < len(p.tree.containers) { p.lines.backup() doCodeBlock() return } if m := codeFenceCloserRegexp.FindStringSubmatch(line); m != nil { closer := m[1] if closer[0] == opener[0] && len(closer) >= len(opener) { doCodeBlock() return } } for i := indent; i > 0 && line != "" && line[0] == ' '; i-- { line = line[1:] } lines = append(lines, line) } doCodeBlock() } // Code fence info strings are mostly verbatim, but support backslash and // entities. This mirrors part of (*inlineParser).render. func processCodeFenceInfo(text string) string { pos := 0 var sb strings.Builder for pos < len(text) { b := text[pos] if b == '&' { if entity := leadingCharRef(text[pos:]); entity != "" { sb.WriteString(UnescapeHTML(entity)) pos += len(entity) continue } } else if b == '\\' && pos+1 < len(text) && isASCIIPunct(text[pos+1]) { b = text[pos+1] pos++ } sb.WriteByte(b) pos++ } return sb.String() } func (p *blockParser) parseIndentedCodeBlock(line string) { lines := []string{strings.TrimPrefix(line, indentedCodePrefix)} startLineNo := p.lines.lastLineNo doCodeBlock := func() { p.codec.Do(Op{Type: OpCodeBlock, LineNo: startLineNo, Lines: lines}) } var savedBlankLines []string for p.lines.more() { line, lineNo := p.lines.next() line, matchedContainers := p.tree.matchContinuationMarkers(line) if isBlankLine(line) { if i, unmatched := p.tree.unmatchedBlockquote(matchedContainers); unmatched { doCodeBlock() p.tree.closeBlocks(i, lineNo, p.codec) return } if strings.HasPrefix(line, indentedCodePrefix) { line = strings.TrimPrefix(line, indentedCodePrefix) } else { line = "" } savedBlankLines = append(savedBlankLines, line) continue } else if matchedContainers < len(p.tree.containers) || !strings.HasPrefix(line, indentedCodePrefix) { p.lines.backup() break } lines = append(lines, savedBlankLines...) savedBlankLines = savedBlankLines[:0] lines = append(lines, strings.TrimPrefix(line, indentedCodePrefix)) } doCodeBlock() } func (p *blockParser) parseCloserTerminatedHTMLBlock(line string, closer func(string) bool) { lines := []string{line} startLineNo := p.lines.lastLineNo doHTMLBlock := func() { p.codec.Do(Op{Type: OpHTMLBlock, LineNo: startLineNo, Lines: lines}) } if closer(line) { doHTMLBlock() return } var savedBlankLines []string for p.lines.more() { line, lineNo := p.lines.next() line, matchedContainers := p.tree.matchContinuationMarkers(line) if isBlankLine(line) { if i, unmatched := p.tree.unmatchedBlockquote(matchedContainers); unmatched { doHTMLBlock() p.tree.closeBlocks(i, lineNo, p.codec) return } savedBlankLines = append(savedBlankLines, line) continue } else if matchedContainers < len(p.tree.containers) { p.lines.backup() doHTMLBlock() return } lines = append(lines, savedBlankLines...) savedBlankLines = savedBlankLines[:0] lines = append(lines, line) if closer(line) { doHTMLBlock() return } } doHTMLBlock() } func (p *blockParser) parseBlankLineTerminatedHTMLBlock(line string) { lines := []string{line} startLineNo := p.lines.lastLineNo doHTMLBlock := func() { p.codec.Do(Op{Type: OpHTMLBlock, LineNo: startLineNo, Lines: lines}) } for p.lines.more() { line, lineNo := p.lines.next() line, matchedContainers := p.tree.matchContinuationMarkers(line) if isBlankLine(line) { doHTMLBlock() if i, unmatched := p.tree.unmatchedBlockquote(matchedContainers); unmatched { p.tree.closeBlocks(i, lineNo, p.codec) } return } else if matchedContainers < len(p.tree.containers) { p.lines.backup() break } lines = append(lines, line) } doHTMLBlock() } // This struct corresponds to the block tree in // https://spec.commonmark.org/0.31.2/#phase-1-block-structure. // // The spec describes a two-phased parsing strategy where the entire block tree // is built before inline parsing is done. However, since we don't support // setext headings and link reference definitions, and treats all lists as // loose, the rendering result of closed blocks will never be impacted by future // blocks. This enables us to render as we parse, and allows us to only track // the path of currently open blocks, which is the same as the rightmost path in // the full block tree at any given point in time. // // The path consists of zero or more container nodes, and an optional paragraph // node. The paragraph node exists if and only if it contains at least 1 line; // the spec prohibits paragraphs consisting of 0 lines. // // We don't need to track any other type of leaf blocks, because they all have // simple termination conditions, so can be parsed in one iteration of the main // parsing loop, as a nested loop that consumes lines until the block // terminates. // // Paragraphs, however, don't have a simple termination condition. Other than // the common condition of being terminated as part of the container block, // paragraphs are always terminated by *another* type of leaf block. This means // that the logic for deciding to continue or interrupt of a paragraph lives // within the main parsing loop. This in turn makes it necessary to store the // lines of the paragraph across iterations of the main parsing loop, hence part // of the parser's state. type blockTree struct { containers []container paragraph []string } // Processes container markers at the start of the line, which consists of // continuation markers of existing containers and starting markers of new // containers. // // Returns the line after removing both types of markers, the number of markers // matched or parsed, and whether the innermost container is a newly opened list // item. // // The latter should be used to call t.closeContainers // unless the remaining content of the line constitutes a blank line or // paragraph continuation. func (t *blockTree) processContainerMarkers(line string, lineNo int, codec Codec) (string, int, bool) { line, matched := t.matchContinuationMarkers(line) line, newContainers := t.parseStartingMarkers(line, // This argument tells parseStartingMarkers whether we are starting a // new paragraph. This seems straightforward enough: if the paragraph is // empty is the first place, or if we are going to terminate some // containers, we are starting a new paragraph. // // The second part of the condition is more subtle though. If the // remaining content of the line constitutes paragraph continuation, we // are not starting a new paragraph. We are only able to ignore this // case parseStartingMarkers only uses this condition when it actually // parses a starting marker, meaning that the line cannot be paragraph // continuation. len(t.paragraph) == 0 || matched != len(t.containers)) continueList := false if matched > 0 && t.containers[matched-1].typ.isList() { // If the last matched container is a list (i.e. the first unmatched // container is a list item), keep it if and only if the first // container to add is a list item that can continue the list. continueList = len(newContainers) > 0 && newContainers[0].punct == t.containers[matched-1].punct if !continueList { matched-- } } if len(newContainers) == 0 { return line, matched, false } t.closeBlocks(matched, lineNo, codec) for _, c := range newContainers { if c.typ.isItem() { if continueList { continueList = false } else { list := container{typ: c.typ.itemToList(), punct: c.punct, start: c.start} t.containers = append(t.containers, list) codec.Do(Op{Type: containerOpenOp[list.typ], LineNo: lineNo, Number: list.start}) } } t.containers = append(t.containers, c) codec.Do(Op{Type: containerOpenOp[c.typ], LineNo: lineNo}) } return line, len(t.containers), newContainers[len(newContainers)-1].typ.isItem() } // Matches the continuation markers of existing container nodes. Returns the // line after removing all matched continuation markers and the number of // containers matched. func (t *blockTree) matchContinuationMarkers(line string) (string, int) { for i, container := range t.containers { markerLen, matched := container.matchContinuationMarker(line) if !matched { return line, i } line = line[markerLen:] } return line, len(t.containers) } // Finds the first blockquote container after skipping matched containers. // Returns len(t.containers), false if not found. // // This is used for handling blank lines. Blank lines do not close list item // blocks (except when a blank line follows a list item starting with a blank // item), but they do close blockquote blocks if the continuation marker is // missing. func (t *blockTree) unmatchedBlockquote(matched int) (int, bool) { for i := matched; i < len(t.containers); i++ { if t.containers[i].typ == blockquote { return i, true } } return len(t.containers), false } var ( // https://spec.commonmark.org/0.31.2/#block-quotes blockquoteMarkerRegexp = regexp.MustCompile(`^ {0,3}> ?`) // Rule #1 and #2 of https://spec.commonmark.org/0.31.2/#list-items itemStartingMarkerRegexp = regexp.MustCompile( // Capture groups: // 1. bullet item punctuation // 2. ordered item start index // 3. ordered item punctuation // 4. trailing spaces `^ {0,3}(?:([-+*])|([0-9]{1,9})([.)]))( +)`) // Rule #3 of https://spec.commonmark.org/0.31.2/#list-items itemStartingMarkerBlankLineRegexp = regexp.MustCompile( // Capture groups are the same, with group 4 always empty. `^ {0,3}(?:([-+*])|([0-9]{1,9})([.)]))[ \t]*()$`) ) // Parses starting markers of container blocks. Returns the line after removing // all starting markers and new containers to create. // // Blockquotes are simple to parse. Most of the code deals with list items, // described in https://spec.commonmark.org/0.31.2/#list-items. func (t *blockTree) parseStartingMarkers(line string, newParagraph bool) (string, []container) { var containers []container // Exception 2 of rule #1: Don't parse thematic breaks like "- - - " as // three bullets. for !thematicBreakRegexp.MatchString(line) { if bqMarker := blockquoteMarkerRegexp.FindString(line); bqMarker != "" { line = line[len(bqMarker):] containers = append(containers, container{typ: blockquote}) continue } m := itemStartingMarkerRegexp.FindStringSubmatch(line) if m == nil && newParagraph { m = itemStartingMarkerBlankLineRegexp.FindStringSubmatch(line) } if m == nil { break } marker, bulletPunct, orderedStart, orderedPunct, spaces := m[0], m[1], m[2], m[3], m[4] if len(spaces) >= 5 { // Rule #2 applies; only the first space is as part of the marker. marker = marker[:len(marker)-len(spaces)+1] } indent := len(marker) if strings.Trim(line[len(marker):], " \t") == "" { // Rule #3 applies: indent is exactly one space, regardless of how // many spaces there actually are, which can be 0. indent = len(strings.TrimRight(marker, " \t")) + 1 } c := container{continuation: strings.Repeat(" ", indent)} if bulletPunct != "" { c.typ = bulletItem c.punct = bulletPunct[0] } else { c.typ = orderedItem c.punct = orderedPunct[0] c.start, _ = strconv.Atoi(orderedStart) if c.start != 1 && !newParagraph { break } } line = line[len(marker):] containers = append(containers, c) // After parsing at least one starting marker, the rest of the line is // in a new paragraph. This means that bullet list marker can be // terminated by end of line or tab (instead of space), and ordered list // marker with number != 1 are allowed. newParagraph = true } return line, containers } func (t *blockTree) closeBlocks(keep, lineNo int, codec Codec) { t.closeParagraph(lineNo, codec) for i := len(t.containers) - 1; i >= keep; i-- { codec.Do(Op{Type: containerCloseOp[t.containers[i].typ], LineNo: lineNo}) } t.containers = t.containers[:keep] } // lineNo identifies the first line not part of the paragraph. func (t *blockTree) closeParagraph(lineNo int, codec Codec) { if len(t.paragraph) == 0 { return } startLineNo := lineNo - len(t.paragraph) text := strings.Trim(strings.Join(t.paragraph, "\n"), " \t") t.paragraph = t.paragraph[:0] codec.Do(Op{Type: OpParagraph, LineNo: startLineNo, Content: renderInline(text)}) } type container struct { typ containerType punct byte start int continuation string } type containerType uint8 const ( blockquote containerType = iota bulletList bulletItem orderedList orderedItem ) func (t containerType) isList() bool { return t == bulletList || t == orderedList } func (t containerType) isItem() bool { return t == bulletItem || t == orderedItem } func (t containerType) itemToList() containerType { if t == bulletItem { return bulletList } else { return orderedList } } var ( containerOpenOp = []OpType{ blockquote: OpBlockquoteStart, bulletList: OpBulletListStart, bulletItem: OpListItemStart, orderedList: OpOrderedListStart, orderedItem: OpListItemStart, } containerCloseOp = []OpType{ blockquote: OpBlockquoteEnd, bulletList: OpBulletListEnd, bulletItem: OpListItemEnd, orderedList: OpOrderedListEnd, orderedItem: OpListItemEnd, } ) func (c container) matchContinuationMarker(line string) (int, bool) { switch c.typ { case blockquote: marker := blockquoteMarkerRegexp.FindString(line) return len(marker), marker != "" case bulletList, orderedList: return 0, true case bulletItem, orderedItem: if strings.HasPrefix(line, c.continuation) { return len(c.continuation), true } return 0, false } panic("unreachable") } // Provides support for consuming a string line by line. type lineSplitter struct { text string pos int // Line number of the last line returned by next. lastLineNo int } func (s *lineSplitter) more() bool { return s.pos < len(s.text) } func (s *lineSplitter) next() (string, int) { begin := s.pos delta := strings.IndexByte(s.text[begin:], '\n') if delta == -1 { s.pos = len(s.text) s.lastLineNo++ return s.text[begin:], s.lastLineNo } s.pos += delta + 1 s.lastLineNo++ return s.text[begin : s.pos-1], s.lastLineNo } func (s *lineSplitter) backup() { if s.pos == 0 { return } s.pos = 1 + strings.LastIndexByte(s.text[:s.pos-1], '\n') s.lastLineNo-- } var leftAnchoredCharRefRegexp = regexp.MustCompile(`^` + charRefPattern) func leadingCharRef(s string) string { return leftAnchoredCharRefRegexp.FindString(s) } elvish-0.21.0/pkg/md/md_test.go000066400000000000000000000026031465720375400162510ustar00rootroot00000000000000package md_test import ( "fmt" "strings" "testing" "src.elv.sh/pkg/diff" "src.elv.sh/pkg/md" "src.elv.sh/pkg/testutil" ) // Most of the parser behavior is tested indirectly via the HTML output. This // file only covers behavior not observable from the HTML output. type lineNoCodec struct{ strings.Builder } func (c *lineNoCodec) Do(op md.Op) { fmt.Fprintln(&c.Builder, op.Type, op.LineNo) } var lineNoTestInput = testutil.Dedent(` --- # line 3 ~~~line 5 foo ~~~ line 9 foo
    line 12
    	

    line 15 line 17 more lines > line 20 > more lines > > line 23 - line 25 line 27 - line 29 1. line 31 2. line 33 `) var lineNoTestOutput = testutil.Dedent(` OpThematicBreak 1 OpHeading 3 OpCodeBlock 5 OpCodeBlock 9 OpHTMLBlock 12 OpHTMLBlock 15 OpParagraph 17 OpBlockquoteStart 20 OpParagraph 20 OpParagraph 23 OpBlockquoteEnd 24 OpBulletListStart 25 OpListItemStart 25 OpParagraph 25 OpParagraph 27 OpListItemEnd 29 OpListItemStart 29 OpParagraph 29 OpListItemEnd 31 OpBulletListEnd 31 OpOrderedListStart 31 OpListItemStart 31 OpParagraph 31 OpListItemEnd 33 OpListItemStart 33 OpParagraph 33 OpListItemEnd 34 OpOrderedListEnd 34 `) func TestLineNo(t *testing.T) { got := md.RenderString(lineNoTestInput, &lineNoCodec{}) if want := lineNoTestOutput; want != got { t.Errorf("%s", diff.Diff("want", want, "got", got)) } } elvish-0.21.0/pkg/md/mdrun/000077500000000000000000000000001465720375400154075ustar00rootroot00000000000000elvish-0.21.0/pkg/md/mdrun/main.go000066400000000000000000000024511465720375400166640ustar00rootroot00000000000000// Command mdrun can be used to test the md package. Run it with "go run". package main import ( "flag" "fmt" "io" "os" "runtime/pprof" "src.elv.sh/pkg/md" ) var ( cpuprofile = flag.String("cpuprofile", "", "name of file to store CPU profile in") codec = flag.String("codec", "html", "codec to use; one of html, trace, fmt, tty") width = flag.Int("width", 0, "text width; relevant with fmt or tty") ) func main() { flag.Parse() c := getCodec(*codec) bs, err := io.ReadAll(os.Stdin) if err != nil { fmt.Fprintln(os.Stderr, "read stdin:", err) os.Exit(2) } if *cpuprofile != "" { f, err := os.OpenFile(*cpuprofile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { fmt.Printf("create cpu profile file %q: %v\n", *cpuprofile, err) os.Exit(2) } defer f.Close() err = pprof.StartCPUProfile(f) if err != nil { fmt.Println("start cpu profile:", err) os.Exit(2) } defer pprof.StopCPUProfile() } fmt.Print(md.RenderString(string(bs), c)) } func getCodec(s string) md.StringerCodec { switch *codec { case "html": return &md.HTMLCodec{} case "trace": return &md.TraceCodec{} case "fmt": return &md.FmtCodec{Width: *width} case "tty": return &md.TTYCodec{Width: *width} default: fmt.Println("unknown codec:", s) os.Exit(2) return nil } } elvish-0.21.0/pkg/md/smart_puncts.go000066400000000000000000000036541465720375400173430ustar00rootroot00000000000000package md import ( "strings" "unicode" ) // SmartPunctsCodec wraps another codec, converting certain ASCII punctuations to // nicer Unicode counterparts: // // - A straight double quote (") is converted to a left double quote (“) when // it follows a whitespace, or a right double quote (”) when it follows a // non-whitespace. // // - A straight single quote (') is converted to a left single quote (‘) when // it follows a whitespace, or a right single quote or apostrophe (’) when // it follows a non-whitespace. // // - A run of two dashes (--) is converted to an en-dash (–). // // - A run of three dashes (---) is converted to an em-dash (—). // // - A run of three dot (...) is converted to an ellipsis (…). // // Start of lines are considered to be whitespaces. type SmartPunctsCodec struct{ Inner Codec } func (c SmartPunctsCodec) Do(op Op) { c.Inner.Do(applySmartPunctsToOp(op)) } func applySmartPunctsToOp(op Op) Op { for i := range op.Content { inlineOp := &op.Content[i] switch inlineOp.Type { case OpText, OpLinkStart, OpLinkEnd, OpImage: inlineOp.Text = applySmartPuncts(inlineOp.Text) if inlineOp.Type == OpImage { inlineOp.Alt = applySmartPuncts(inlineOp.Alt) } } } return op } var applySimpleSmartPuncts = strings.NewReplacer( "--", "–", "---", "—", "...", "…").Replace func applySmartPuncts(s string) string { return applySimpleSmartPuncts(applySmartQuotes(s)) } func applySmartQuotes(s string) string { if !strings.ContainsAny(s, `'"`) { return s } var sb strings.Builder // Start of line is considered to be whitespace prev := ' ' for _, r := range s { if r == '"' { if unicode.IsSpace(prev) { sb.WriteRune('“') } else { sb.WriteRune('”') } } else if r == '\'' { if unicode.IsSpace(prev) { sb.WriteRune('‘') } else { sb.WriteRune('’') } } else { sb.WriteRune(r) } prev = r } return sb.String() } elvish-0.21.0/pkg/md/smart_puncts_test.go000066400000000000000000000026631465720375400204010ustar00rootroot00000000000000package md_test import ( "testing" "github.com/google/go-cmp/cmp" . "src.elv.sh/pkg/md" ) var smartPunctsTestCases = []testCase{ { Name: "Simple smart punctuations", Markdown: `a -- b --- c...`, HTML: dedent(`

    a – b –- c…

    `), }, { Name: "Smart quotes", Markdown: `It's "foo" and 'bar'.`, HTML: dedent(`

    It’s “foo” and ‘bar’.

    `), }, { Name: "Link and image title", Markdown: dedent(` [link text](a.html "--") ![img alt](a.png "--") `), HTML: dedent(`

    link text img alt

    `), }, { Name: "Link alt", Markdown: `![img -- alt](a.png)`, HTML: dedent(`

    img – alt

    `), }, { Name: "Code span is unchanged", Markdown: "`a -- b`", HTML: dedent(`

    a -- b

    `), }, { Name: "Non-inline content is unchanged", Markdown: dedent(` ~~~ a -- b ~~~ `), HTML: dedent(`
    a -- b
    			
    `), }, } func TestSmartPuncts(t *testing.T) { for _, tc := range smartPunctsTestCases { t.Run(tc.Name, func(t *testing.T) { var htmlCodec HTMLCodec Render(tc.Markdown, SmartPunctsCodec{&htmlCodec}) got := htmlCodec.String() if diff := cmp.Diff(tc.HTML, got); diff != "" { t.Errorf("input:\n%s\ndiff (-want +got):\n%s", hr+"\n"+tc.Markdown+hr, diff) } }) } } elvish-0.21.0/pkg/md/spec/000077500000000000000000000000001465720375400152145ustar00rootroot00000000000000elvish-0.21.0/pkg/md/spec/LICENSE000066400000000000000000000003521465720375400162210ustar00rootroot00000000000000The spec tests (spec.json) are derived from the CommonMark spec (spec.txt), which are Copyright (C) 2014-16 John MacFarlane Released under the Creative Commons CC-BY-SA 4.0 license: . elvish-0.21.0/pkg/md/spec/spec.json000066400000000000000000004223071465720375400170510ustar00rootroot00000000000000[ { "markdown": "\tfoo\tbaz\t\tbim\n", "html": "
    foo\tbaz\t\tbim\n
    \n", "example": 1, "start_line": 355, "end_line": 360, "section": "Tabs" }, { "markdown": " \tfoo\tbaz\t\tbim\n", "html": "
    foo\tbaz\t\tbim\n
    \n", "example": 2, "start_line": 362, "end_line": 367, "section": "Tabs" }, { "markdown": " a\ta\n ὐ\ta\n", "html": "
    a\ta\nὐ\ta\n
    \n", "example": 3, "start_line": 369, "end_line": 376, "section": "Tabs" }, { "markdown": " - foo\n\n\tbar\n", "html": "
      \n
    • \n

      foo

      \n

      bar

      \n
    • \n
    \n", "example": 4, "start_line": 382, "end_line": 393, "section": "Tabs" }, { "markdown": "- foo\n\n\t\tbar\n", "html": "
      \n
    • \n

      foo

      \n
        bar\n
      \n
    • \n
    \n", "example": 5, "start_line": 395, "end_line": 407, "section": "Tabs" }, { "markdown": ">\t\tfoo\n", "html": "
    \n
      foo\n
    \n
    \n", "example": 6, "start_line": 418, "end_line": 425, "section": "Tabs" }, { "markdown": "-\t\tfoo\n", "html": "
      \n
    • \n
        foo\n
      \n
    • \n
    \n", "example": 7, "start_line": 427, "end_line": 436, "section": "Tabs" }, { "markdown": " foo\n\tbar\n", "html": "
    foo\nbar\n
    \n", "example": 8, "start_line": 439, "end_line": 446, "section": "Tabs" }, { "markdown": " - foo\n - bar\n\t - baz\n", "html": "
      \n
    • foo\n
        \n
      • bar\n
          \n
        • baz
        • \n
        \n
      • \n
      \n
    • \n
    \n", "example": 9, "start_line": 448, "end_line": 464, "section": "Tabs" }, { "markdown": "#\tFoo\n", "html": "

    Foo

    \n", "example": 10, "start_line": 466, "end_line": 470, "section": "Tabs" }, { "markdown": "*\t*\t*\t\n", "html": "
    \n", "example": 11, "start_line": 472, "end_line": 476, "section": "Tabs" }, { "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", "html": "

    !"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~

    \n", "example": 12, "start_line": 489, "end_line": 493, "section": "Backslash escapes" }, { "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", "html": "

    \\\t\\A\\a\\ \\3\\φ\\«

    \n", "example": 13, "start_line": 499, "end_line": 503, "section": "Backslash escapes" }, { "markdown": "\\*not emphasized*\n\\
    not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity\n", "html": "

    *not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity

    \n", "example": 14, "start_line": 509, "end_line": 529, "section": "Backslash escapes" }, { "markdown": "\\\\*emphasis*\n", "html": "

    \\emphasis

    \n", "example": 15, "start_line": 534, "end_line": 538, "section": "Backslash escapes" }, { "markdown": "foo\\\nbar\n", "html": "

    foo
    \nbar

    \n", "example": 16, "start_line": 543, "end_line": 549, "section": "Backslash escapes" }, { "markdown": "`` \\[\\` ``\n", "html": "

    \\[\\`

    \n", "example": 17, "start_line": 555, "end_line": 559, "section": "Backslash escapes" }, { "markdown": " \\[\\]\n", "html": "
    \\[\\]\n
    \n", "example": 18, "start_line": 562, "end_line": 567, "section": "Backslash escapes" }, { "markdown": "~~~\n\\[\\]\n~~~\n", "html": "
    \\[\\]\n
    \n", "example": 19, "start_line": 570, "end_line": 577, "section": "Backslash escapes" }, { "markdown": "\n", "html": "

    https://example.com?find=\\*

    \n", "example": 20, "start_line": 580, "end_line": 584, "section": "Backslash escapes" }, { "markdown": "\n", "html": "\n", "example": 21, "start_line": 587, "end_line": 591, "section": "Backslash escapes" }, { "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", "html": "

    foo

    \n", "example": 22, "start_line": 597, "end_line": 601, "section": "Backslash escapes" }, { "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n", "html": "

    foo

    \n", "example": 23, "start_line": 604, "end_line": 610, "section": "Backslash escapes" }, { "markdown": "``` foo\\+bar\nfoo\n```\n", "html": "
    foo\n
    \n", "example": 24, "start_line": 613, "end_line": 620, "section": "Backslash escapes" }, { "markdown": "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", "html": "

      & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸

    \n", "example": 25, "start_line": 649, "end_line": 657, "section": "Entity and numeric character references" }, { "markdown": "# Ӓ Ϡ �\n", "html": "

    # Ӓ Ϡ �

    \n", "example": 26, "start_line": 668, "end_line": 672, "section": "Entity and numeric character references" }, { "markdown": "" ആ ಫ\n", "html": "

    " ആ ಫ

    \n", "example": 27, "start_line": 681, "end_line": 685, "section": "Entity and numeric character references" }, { "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", "html": "

    &nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;

    \n", "example": 28, "start_line": 690, "end_line": 700, "section": "Entity and numeric character references" }, { "markdown": "©\n", "html": "

    &copy

    \n", "example": 29, "start_line": 707, "end_line": 711, "section": "Entity and numeric character references" }, { "markdown": "&MadeUpEntity;\n", "html": "

    &MadeUpEntity;

    \n", "example": 30, "start_line": 717, "end_line": 721, "section": "Entity and numeric character references" }, { "markdown": "\n", "html": "\n", "example": 31, "start_line": 728, "end_line": 732, "section": "Entity and numeric character references" }, { "markdown": "[foo](/föö \"föö\")\n", "html": "

    foo

    \n", "example": 32, "start_line": 735, "end_line": 739, "section": "Entity and numeric character references" }, { "markdown": "[foo]\n\n[foo]: /föö \"föö\"\n", "html": "

    foo

    \n", "example": 33, "start_line": 742, "end_line": 748, "section": "Entity and numeric character references" }, { "markdown": "``` föö\nfoo\n```\n", "html": "
    foo\n
    \n", "example": 34, "start_line": 751, "end_line": 758, "section": "Entity and numeric character references" }, { "markdown": "`föö`\n", "html": "

    f&ouml;&ouml;

    \n", "example": 35, "start_line": 764, "end_line": 768, "section": "Entity and numeric character references" }, { "markdown": " föfö\n", "html": "
    f&ouml;f&ouml;\n
    \n", "example": 36, "start_line": 771, "end_line": 776, "section": "Entity and numeric character references" }, { "markdown": "*foo*\n*foo*\n", "html": "

    *foo*\nfoo

    \n", "example": 37, "start_line": 783, "end_line": 789, "section": "Entity and numeric character references" }, { "markdown": "* foo\n\n* foo\n", "html": "

    * foo

    \n
      \n
    • foo
    • \n
    \n", "example": 38, "start_line": 791, "end_line": 800, "section": "Entity and numeric character references" }, { "markdown": "foo bar\n", "html": "

    foo\n\nbar

    \n", "example": 39, "start_line": 802, "end_line": 808, "section": "Entity and numeric character references" }, { "markdown": " foo\n", "html": "

    \tfoo

    \n", "example": 40, "start_line": 810, "end_line": 814, "section": "Entity and numeric character references" }, { "markdown": "[a](url "tit")\n", "html": "

    [a](url "tit")

    \n", "example": 41, "start_line": 817, "end_line": 821, "section": "Entity and numeric character references" }, { "markdown": "- `one\n- two`\n", "html": "
      \n
    • `one
    • \n
    • two`
    • \n
    \n", "example": 42, "start_line": 840, "end_line": 848, "section": "Precedence" }, { "markdown": "***\n---\n___\n", "html": "
    \n
    \n
    \n", "example": 43, "start_line": 879, "end_line": 887, "section": "Thematic breaks" }, { "markdown": "+++\n", "html": "

    +++

    \n", "example": 44, "start_line": 892, "end_line": 896, "section": "Thematic breaks" }, { "markdown": "===\n", "html": "

    ===

    \n", "example": 45, "start_line": 899, "end_line": 903, "section": "Thematic breaks" }, { "markdown": "--\n**\n__\n", "html": "

    --\n**\n__

    \n", "example": 46, "start_line": 908, "end_line": 916, "section": "Thematic breaks" }, { "markdown": " ***\n ***\n ***\n", "html": "
    \n
    \n
    \n", "example": 47, "start_line": 921, "end_line": 929, "section": "Thematic breaks" }, { "markdown": " ***\n", "html": "
    ***\n
    \n", "example": 48, "start_line": 934, "end_line": 939, "section": "Thematic breaks" }, { "markdown": "Foo\n ***\n", "html": "

    Foo\n***

    \n", "example": 49, "start_line": 942, "end_line": 948, "section": "Thematic breaks" }, { "markdown": "_____________________________________\n", "html": "
    \n", "example": 50, "start_line": 953, "end_line": 957, "section": "Thematic breaks" }, { "markdown": " - - -\n", "html": "
    \n", "example": 51, "start_line": 962, "end_line": 966, "section": "Thematic breaks" }, { "markdown": " ** * ** * ** * **\n", "html": "
    \n", "example": 52, "start_line": 969, "end_line": 973, "section": "Thematic breaks" }, { "markdown": "- - - -\n", "html": "
    \n", "example": 53, "start_line": 976, "end_line": 980, "section": "Thematic breaks" }, { "markdown": "- - - - \n", "html": "
    \n", "example": 54, "start_line": 985, "end_line": 989, "section": "Thematic breaks" }, { "markdown": "_ _ _ _ a\n\na------\n\n---a---\n", "html": "

    _ _ _ _ a

    \n

    a------

    \n

    ---a---

    \n", "example": 55, "start_line": 994, "end_line": 1004, "section": "Thematic breaks" }, { "markdown": " *-*\n", "html": "

    -

    \n", "example": 56, "start_line": 1010, "end_line": 1014, "section": "Thematic breaks" }, { "markdown": "- foo\n***\n- bar\n", "html": "
      \n
    • foo
    • \n
    \n
    \n
      \n
    • bar
    • \n
    \n", "example": 57, "start_line": 1019, "end_line": 1031, "section": "Thematic breaks" }, { "markdown": "Foo\n***\nbar\n", "html": "

    Foo

    \n
    \n

    bar

    \n", "example": 58, "start_line": 1036, "end_line": 1044, "section": "Thematic breaks" }, { "markdown": "Foo\n---\nbar\n", "html": "

    Foo

    \n

    bar

    \n", "example": 59, "start_line": 1053, "end_line": 1060, "section": "Thematic breaks" }, { "markdown": "* Foo\n* * *\n* Bar\n", "html": "
      \n
    • Foo
    • \n
    \n
    \n
      \n
    • Bar
    • \n
    \n", "example": 60, "start_line": 1066, "end_line": 1078, "section": "Thematic breaks" }, { "markdown": "- Foo\n- * * *\n", "html": "
      \n
    • Foo
    • \n
    • \n
      \n
    • \n
    \n", "example": 61, "start_line": 1083, "end_line": 1093, "section": "Thematic breaks" }, { "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n", "html": "

    foo

    \n

    foo

    \n

    foo

    \n

    foo

    \n
    foo
    \n
    foo
    \n", "example": 62, "start_line": 1112, "end_line": 1126, "section": "ATX headings" }, { "markdown": "####### foo\n", "html": "

    ####### foo

    \n", "example": 63, "start_line": 1131, "end_line": 1135, "section": "ATX headings" }, { "markdown": "#5 bolt\n\n#hashtag\n", "html": "

    #5 bolt

    \n

    #hashtag

    \n", "example": 64, "start_line": 1146, "end_line": 1153, "section": "ATX headings" }, { "markdown": "\\## foo\n", "html": "

    ## foo

    \n", "example": 65, "start_line": 1158, "end_line": 1162, "section": "ATX headings" }, { "markdown": "# foo *bar* \\*baz\\*\n", "html": "

    foo bar *baz*

    \n", "example": 66, "start_line": 1167, "end_line": 1171, "section": "ATX headings" }, { "markdown": "# foo \n", "html": "

    foo

    \n", "example": 67, "start_line": 1176, "end_line": 1180, "section": "ATX headings" }, { "markdown": " ### foo\n ## foo\n # foo\n", "html": "

    foo

    \n

    foo

    \n

    foo

    \n", "example": 68, "start_line": 1185, "end_line": 1193, "section": "ATX headings" }, { "markdown": " # foo\n", "html": "
    # foo\n
    \n", "example": 69, "start_line": 1198, "end_line": 1203, "section": "ATX headings" }, { "markdown": "foo\n # bar\n", "html": "

    foo\n# bar

    \n", "example": 70, "start_line": 1206, "end_line": 1212, "section": "ATX headings" }, { "markdown": "## foo ##\n ### bar ###\n", "html": "

    foo

    \n

    bar

    \n", "example": 71, "start_line": 1217, "end_line": 1223, "section": "ATX headings" }, { "markdown": "# foo ##################################\n##### foo ##\n", "html": "

    foo

    \n
    foo
    \n", "example": 72, "start_line": 1228, "end_line": 1234, "section": "ATX headings" }, { "markdown": "### foo ### \n", "html": "

    foo

    \n", "example": 73, "start_line": 1239, "end_line": 1243, "section": "ATX headings" }, { "markdown": "### foo ### b\n", "html": "

    foo ### b

    \n", "example": 74, "start_line": 1250, "end_line": 1254, "section": "ATX headings" }, { "markdown": "# foo#\n", "html": "

    foo#

    \n", "example": 75, "start_line": 1259, "end_line": 1263, "section": "ATX headings" }, { "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n", "html": "

    foo ###

    \n

    foo ###

    \n

    foo #

    \n", "example": 76, "start_line": 1269, "end_line": 1277, "section": "ATX headings" }, { "markdown": "****\n## foo\n****\n", "html": "
    \n

    foo

    \n
    \n", "example": 77, "start_line": 1283, "end_line": 1291, "section": "ATX headings" }, { "markdown": "Foo bar\n# baz\nBar foo\n", "html": "

    Foo bar

    \n

    baz

    \n

    Bar foo

    \n", "example": 78, "start_line": 1294, "end_line": 1302, "section": "ATX headings" }, { "markdown": "## \n#\n### ###\n", "html": "

    \n

    \n

    \n", "example": 79, "start_line": 1307, "end_line": 1315, "section": "ATX headings" }, { "markdown": "Foo *bar*\n=========\n\nFoo *bar*\n---------\n", "html": "

    Foo bar

    \n

    Foo bar

    \n", "example": 80, "start_line": 1347, "end_line": 1356, "section": "Setext headings" }, { "markdown": "Foo *bar\nbaz*\n====\n", "html": "

    Foo bar\nbaz

    \n", "example": 81, "start_line": 1361, "end_line": 1368, "section": "Setext headings" }, { "markdown": " Foo *bar\nbaz*\t\n====\n", "html": "

    Foo bar\nbaz

    \n", "example": 82, "start_line": 1375, "end_line": 1382, "section": "Setext headings" }, { "markdown": "Foo\n-------------------------\n\nFoo\n=\n", "html": "

    Foo

    \n

    Foo

    \n", "example": 83, "start_line": 1387, "end_line": 1396, "section": "Setext headings" }, { "markdown": " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n", "html": "

    Foo

    \n

    Foo

    \n

    Foo

    \n", "example": 84, "start_line": 1402, "end_line": 1415, "section": "Setext headings" }, { "markdown": " Foo\n ---\n\n Foo\n---\n", "html": "
    Foo\n---\n\nFoo\n
    \n
    \n", "example": 85, "start_line": 1420, "end_line": 1433, "section": "Setext headings" }, { "markdown": "Foo\n ---- \n", "html": "

    Foo

    \n", "example": 86, "start_line": 1439, "end_line": 1444, "section": "Setext headings" }, { "markdown": "Foo\n ---\n", "html": "

    Foo\n---

    \n", "example": 87, "start_line": 1449, "end_line": 1455, "section": "Setext headings" }, { "markdown": "Foo\n= =\n\nFoo\n--- -\n", "html": "

    Foo\n= =

    \n

    Foo

    \n
    \n", "example": 88, "start_line": 1460, "end_line": 1471, "section": "Setext headings" }, { "markdown": "Foo \n-----\n", "html": "

    Foo

    \n", "example": 89, "start_line": 1476, "end_line": 1481, "section": "Setext headings" }, { "markdown": "Foo\\\n----\n", "html": "

    Foo\\

    \n", "example": 90, "start_line": 1486, "end_line": 1491, "section": "Setext headings" }, { "markdown": "`Foo\n----\n`\n\n\n", "html": "

    `Foo

    \n

    `

    \n

    <a title="a lot

    \n

    of dashes"/>

    \n", "example": 91, "start_line": 1497, "end_line": 1510, "section": "Setext headings" }, { "markdown": "> Foo\n---\n", "html": "
    \n

    Foo

    \n
    \n
    \n", "example": 92, "start_line": 1516, "end_line": 1524, "section": "Setext headings" }, { "markdown": "> foo\nbar\n===\n", "html": "
    \n

    foo\nbar\n===

    \n
    \n", "example": 93, "start_line": 1527, "end_line": 1537, "section": "Setext headings" }, { "markdown": "- Foo\n---\n", "html": "
      \n
    • Foo
    • \n
    \n
    \n", "example": 94, "start_line": 1540, "end_line": 1548, "section": "Setext headings" }, { "markdown": "Foo\nBar\n---\n", "html": "

    Foo\nBar

    \n", "example": 95, "start_line": 1555, "end_line": 1562, "section": "Setext headings" }, { "markdown": "---\nFoo\n---\nBar\n---\nBaz\n", "html": "
    \n

    Foo

    \n

    Bar

    \n

    Baz

    \n", "example": 96, "start_line": 1568, "end_line": 1580, "section": "Setext headings" }, { "markdown": "\n====\n", "html": "

    ====

    \n", "example": 97, "start_line": 1585, "end_line": 1590, "section": "Setext headings" }, { "markdown": "---\n---\n", "html": "
    \n
    \n", "example": 98, "start_line": 1597, "end_line": 1603, "section": "Setext headings" }, { "markdown": "- foo\n-----\n", "html": "
      \n
    • foo
    • \n
    \n
    \n", "example": 99, "start_line": 1606, "end_line": 1614, "section": "Setext headings" }, { "markdown": " foo\n---\n", "html": "
    foo\n
    \n
    \n", "example": 100, "start_line": 1617, "end_line": 1624, "section": "Setext headings" }, { "markdown": "> foo\n-----\n", "html": "
    \n

    foo

    \n
    \n
    \n", "example": 101, "start_line": 1627, "end_line": 1635, "section": "Setext headings" }, { "markdown": "\\> foo\n------\n", "html": "

    > foo

    \n", "example": 102, "start_line": 1641, "end_line": 1646, "section": "Setext headings" }, { "markdown": "Foo\n\nbar\n---\nbaz\n", "html": "

    Foo

    \n

    bar

    \n

    baz

    \n", "example": 103, "start_line": 1672, "end_line": 1682, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\n---\n\nbaz\n", "html": "

    Foo\nbar

    \n
    \n

    baz

    \n", "example": 104, "start_line": 1688, "end_line": 1700, "section": "Setext headings" }, { "markdown": "Foo\nbar\n* * *\nbaz\n", "html": "

    Foo\nbar

    \n
    \n

    baz

    \n", "example": 105, "start_line": 1706, "end_line": 1716, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\\---\nbaz\n", "html": "

    Foo\nbar\n---\nbaz

    \n", "example": 106, "start_line": 1721, "end_line": 1731, "section": "Setext headings" }, { "markdown": " a simple\n indented code block\n", "html": "
    a simple\n  indented code block\n
    \n", "example": 107, "start_line": 1749, "end_line": 1756, "section": "Indented code blocks" }, { "markdown": " - foo\n\n bar\n", "html": "
      \n
    • \n

      foo

      \n

      bar

      \n
    • \n
    \n", "example": 108, "start_line": 1763, "end_line": 1774, "section": "Indented code blocks" }, { "markdown": "1. foo\n\n - bar\n", "html": "
      \n
    1. \n

      foo

      \n
        \n
      • bar
      • \n
      \n
    2. \n
    \n", "example": 109, "start_line": 1777, "end_line": 1790, "section": "Indented code blocks" }, { "markdown": "
    \n *hi*\n\n - one\n", "html": "
    <a/>\n*hi*\n\n- one\n
    \n", "example": 110, "start_line": 1797, "end_line": 1808, "section": "Indented code blocks" }, { "markdown": " chunk1\n\n chunk2\n \n \n \n chunk3\n", "html": "
    chunk1\n\nchunk2\n\n\n\nchunk3\n
    \n", "example": 111, "start_line": 1813, "end_line": 1830, "section": "Indented code blocks" }, { "markdown": " chunk1\n \n chunk2\n", "html": "
    chunk1\n  \n  chunk2\n
    \n", "example": 112, "start_line": 1836, "end_line": 1845, "section": "Indented code blocks" }, { "markdown": "Foo\n bar\n\n", "html": "

    Foo\nbar

    \n", "example": 113, "start_line": 1851, "end_line": 1858, "section": "Indented code blocks" }, { "markdown": " foo\nbar\n", "html": "
    foo\n
    \n

    bar

    \n", "example": 114, "start_line": 1865, "end_line": 1872, "section": "Indented code blocks" }, { "markdown": "# Heading\n foo\nHeading\n------\n foo\n----\n", "html": "

    Heading

    \n
    foo\n
    \n

    Heading

    \n
    foo\n
    \n
    \n", "example": 115, "start_line": 1878, "end_line": 1893, "section": "Indented code blocks" }, { "markdown": " foo\n bar\n", "html": "
        foo\nbar\n
    \n", "example": 116, "start_line": 1898, "end_line": 1905, "section": "Indented code blocks" }, { "markdown": "\n \n foo\n \n\n", "html": "
    foo\n
    \n", "example": 117, "start_line": 1911, "end_line": 1920, "section": "Indented code blocks" }, { "markdown": " foo \n", "html": "
    foo  \n
    \n", "example": 118, "start_line": 1925, "end_line": 1930, "section": "Indented code blocks" }, { "markdown": "```\n<\n >\n```\n", "html": "
    <\n >\n
    \n", "example": 119, "start_line": 1980, "end_line": 1989, "section": "Fenced code blocks" }, { "markdown": "~~~\n<\n >\n~~~\n", "html": "
    <\n >\n
    \n", "example": 120, "start_line": 1994, "end_line": 2003, "section": "Fenced code blocks" }, { "markdown": "``\nfoo\n``\n", "html": "

    foo

    \n", "example": 121, "start_line": 2007, "end_line": 2013, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n~~~\n```\n", "html": "
    aaa\n~~~\n
    \n", "example": 122, "start_line": 2018, "end_line": 2027, "section": "Fenced code blocks" }, { "markdown": "~~~\naaa\n```\n~~~\n", "html": "
    aaa\n```\n
    \n", "example": 123, "start_line": 2030, "end_line": 2039, "section": "Fenced code blocks" }, { "markdown": "````\naaa\n```\n``````\n", "html": "
    aaa\n```\n
    \n", "example": 124, "start_line": 2044, "end_line": 2053, "section": "Fenced code blocks" }, { "markdown": "~~~~\naaa\n~~~\n~~~~\n", "html": "
    aaa\n~~~\n
    \n", "example": 125, "start_line": 2056, "end_line": 2065, "section": "Fenced code blocks" }, { "markdown": "```\n", "html": "
    \n", "example": 126, "start_line": 2071, "end_line": 2075, "section": "Fenced code blocks" }, { "markdown": "`````\n\n```\naaa\n", "html": "
    \n```\naaa\n
    \n", "example": 127, "start_line": 2078, "end_line": 2088, "section": "Fenced code blocks" }, { "markdown": "> ```\n> aaa\n\nbbb\n", "html": "
    \n
    aaa\n
    \n
    \n

    bbb

    \n", "example": 128, "start_line": 2091, "end_line": 2102, "section": "Fenced code blocks" }, { "markdown": "```\n\n \n```\n", "html": "
    \n  \n
    \n", "example": 129, "start_line": 2107, "end_line": 2116, "section": "Fenced code blocks" }, { "markdown": "```\n```\n", "html": "
    \n", "example": 130, "start_line": 2121, "end_line": 2126, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\naaa\n```\n", "html": "
    aaa\naaa\n
    \n", "example": 131, "start_line": 2133, "end_line": 2142, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n aaa\naaa\n ```\n", "html": "
    aaa\naaa\naaa\n
    \n", "example": 132, "start_line": 2145, "end_line": 2156, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n aaa\n aaa\n ```\n", "html": "
    aaa\n aaa\naaa\n
    \n", "example": 133, "start_line": 2159, "end_line": 2170, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n ```\n", "html": "
    ```\naaa\n```\n
    \n", "example": 134, "start_line": 2175, "end_line": 2184, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "
    aaa\n
    \n", "example": 135, "start_line": 2190, "end_line": 2197, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n ```\n", "html": "
    aaa\n
    \n", "example": 136, "start_line": 2200, "end_line": 2207, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "
    aaa\n    ```\n
    \n", "example": 137, "start_line": 2212, "end_line": 2220, "section": "Fenced code blocks" }, { "markdown": "``` ```\naaa\n", "html": "

    \naaa

    \n", "example": 138, "start_line": 2226, "end_line": 2232, "section": "Fenced code blocks" }, { "markdown": "~~~~~~\naaa\n~~~ ~~\n", "html": "
    aaa\n~~~ ~~\n
    \n", "example": 139, "start_line": 2235, "end_line": 2243, "section": "Fenced code blocks" }, { "markdown": "foo\n```\nbar\n```\nbaz\n", "html": "

    foo

    \n
    bar\n
    \n

    baz

    \n", "example": 140, "start_line": 2249, "end_line": 2260, "section": "Fenced code blocks" }, { "markdown": "foo\n---\n~~~\nbar\n~~~\n# baz\n", "html": "

    foo

    \n
    bar\n
    \n

    baz

    \n", "example": 141, "start_line": 2266, "end_line": 2278, "section": "Fenced code blocks" }, { "markdown": "```ruby\ndef foo(x)\n return 3\nend\n```\n", "html": "
    def foo(x)\n  return 3\nend\n
    \n", "example": 142, "start_line": 2288, "end_line": 2299, "section": "Fenced code blocks" }, { "markdown": "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n", "html": "
    def foo(x)\n  return 3\nend\n
    \n", "example": 143, "start_line": 2302, "end_line": 2313, "section": "Fenced code blocks" }, { "markdown": "````;\n````\n", "html": "
    \n", "example": 144, "start_line": 2316, "end_line": 2321, "section": "Fenced code blocks" }, { "markdown": "``` aa ```\nfoo\n", "html": "

    aa\nfoo

    \n", "example": 145, "start_line": 2326, "end_line": 2332, "section": "Fenced code blocks" }, { "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n", "html": "
    foo\n
    \n", "example": 146, "start_line": 2337, "end_line": 2344, "section": "Fenced code blocks" }, { "markdown": "```\n``` aaa\n```\n", "html": "
    ``` aaa\n
    \n", "example": 147, "start_line": 2349, "end_line": 2356, "section": "Fenced code blocks" }, { "markdown": "
    \n
    \n**Hello**,\n\n_world_.\n
    \n
    \n", "html": "
    \n
    \n**Hello**,\n

    world.\n

    \n
    \n", "example": 148, "start_line": 2428, "end_line": 2443, "section": "HTML blocks" }, { "markdown": "\n \n \n \n
    \n hi\n
    \n\nokay.\n", "html": "\n \n \n \n
    \n hi\n
    \n

    okay.

    \n", "example": 149, "start_line": 2457, "end_line": 2476, "section": "HTML blocks" }, { "markdown": "
    \n*foo*\n", "example": 151, "start_line": 2492, "end_line": 2498, "section": "HTML blocks" }, { "markdown": "
    \n\n*Markdown*\n\n
    \n", "html": "
    \n

    Markdown

    \n
    \n", "example": 152, "start_line": 2503, "end_line": 2513, "section": "HTML blocks" }, { "markdown": "
    \n
    \n", "html": "
    \n
    \n", "example": 153, "start_line": 2519, "end_line": 2527, "section": "HTML blocks" }, { "markdown": "
    \n
    \n", "html": "
    \n
    \n", "example": 154, "start_line": 2530, "end_line": 2538, "section": "HTML blocks" }, { "markdown": "
    \n*foo*\n\n*bar*\n", "html": "
    \n*foo*\n

    bar

    \n", "example": 155, "start_line": 2542, "end_line": 2551, "section": "HTML blocks" }, { "markdown": "
    \n", "html": "\n", "example": 159, "start_line": 2591, "end_line": 2595, "section": "HTML blocks" }, { "markdown": "
    \nfoo\n
    \n", "html": "
    \nfoo\n
    \n", "example": 160, "start_line": 2598, "end_line": 2606, "section": "HTML blocks" }, { "markdown": "
    \n``` c\nint x = 33;\n```\n", "html": "
    \n``` c\nint x = 33;\n```\n", "example": 161, "start_line": 2615, "end_line": 2625, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 162, "start_line": 2632, "end_line": 2640, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 163, "start_line": 2645, "end_line": 2653, "section": "HTML blocks" }, { "markdown": "\n*bar*\n\n", "html": "\n*bar*\n\n", "example": 164, "start_line": 2656, "end_line": 2664, "section": "HTML blocks" }, { "markdown": "\n*bar*\n", "html": "\n*bar*\n", "example": 165, "start_line": 2667, "end_line": 2673, "section": "HTML blocks" }, { "markdown": "\n*foo*\n\n", "html": "\n*foo*\n\n", "example": 166, "start_line": 2682, "end_line": 2690, "section": "HTML blocks" }, { "markdown": "\n\n*foo*\n\n\n", "html": "\n

    foo

    \n
    \n", "example": 167, "start_line": 2697, "end_line": 2707, "section": "HTML blocks" }, { "markdown": "*foo*\n", "html": "

    foo

    \n", "example": 168, "start_line": 2715, "end_line": 2719, "section": "HTML blocks" }, { "markdown": "
    \nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
    \nokay\n", "html": "
    \nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
    \n

    okay

    \n", "example": 169, "start_line": 2731, "end_line": 2747, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

    okay

    \n", "example": 170, "start_line": 2752, "end_line": 2766, "section": "HTML blocks" }, { "markdown": "\n", "html": "\n", "example": 171, "start_line": 2771, "end_line": 2787, "section": "HTML blocks" }, { "markdown": "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n", "html": "\nh1 {color:red;}\n\np {color:blue;}\n\n

    okay

    \n", "example": 172, "start_line": 2791, "end_line": 2807, "section": "HTML blocks" }, { "markdown": "\n\nfoo\n", "html": "\n\nfoo\n", "example": 173, "start_line": 2814, "end_line": 2824, "section": "HTML blocks" }, { "markdown": ">
    \n> foo\n\nbar\n", "html": "
    \n
    \nfoo\n
    \n

    bar

    \n", "example": 174, "start_line": 2827, "end_line": 2838, "section": "HTML blocks" }, { "markdown": "-
    \n- foo\n", "html": "
      \n
    • \n
      \n
    • \n
    • foo
    • \n
    \n", "example": 175, "start_line": 2841, "end_line": 2851, "section": "HTML blocks" }, { "markdown": "\n*foo*\n", "html": "\n

    foo

    \n", "example": 176, "start_line": 2856, "end_line": 2862, "section": "HTML blocks" }, { "markdown": "*bar*\n*baz*\n", "html": "*bar*\n

    baz

    \n", "example": 177, "start_line": 2865, "end_line": 2871, "section": "HTML blocks" }, { "markdown": "1. *bar*\n", "html": "1. *bar*\n", "example": 178, "start_line": 2877, "end_line": 2885, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

    okay

    \n", "example": 179, "start_line": 2890, "end_line": 2902, "section": "HTML blocks" }, { "markdown": "';\n\n?>\nokay\n", "html": "';\n\n?>\n

    okay

    \n", "example": 180, "start_line": 2908, "end_line": 2922, "section": "HTML blocks" }, { "markdown": "\n", "html": "\n", "example": 181, "start_line": 2927, "end_line": 2931, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

    okay

    \n", "example": 182, "start_line": 2936, "end_line": 2964, "section": "HTML blocks" }, { "markdown": " \n\n \n", "html": " \n
    <!-- foo -->\n
    \n", "example": 183, "start_line": 2970, "end_line": 2978, "section": "HTML blocks" }, { "markdown": "
    \n\n
    \n", "html": "
    \n
    <div>\n
    \n", "example": 184, "start_line": 2981, "end_line": 2989, "section": "HTML blocks" }, { "markdown": "Foo\n
    \nbar\n
    \n", "html": "

    Foo

    \n
    \nbar\n
    \n", "example": 185, "start_line": 2995, "end_line": 3005, "section": "HTML blocks" }, { "markdown": "
    \nbar\n
    \n*foo*\n", "html": "
    \nbar\n
    \n*foo*\n", "example": 186, "start_line": 3012, "end_line": 3022, "section": "HTML blocks" }, { "markdown": "Foo\n\nbaz\n", "html": "

    Foo\n\nbaz

    \n", "example": 187, "start_line": 3027, "end_line": 3035, "section": "HTML blocks" }, { "markdown": "
    \n\n*Emphasized* text.\n\n
    \n", "html": "
    \n

    Emphasized text.

    \n
    \n", "example": 188, "start_line": 3068, "end_line": 3078, "section": "HTML blocks" }, { "markdown": "
    \n*Emphasized* text.\n
    \n", "html": "
    \n*Emphasized* text.\n
    \n", "example": 189, "start_line": 3081, "end_line": 3089, "section": "HTML blocks" }, { "markdown": "\n\n\n\n\n\n\n\n
    \nHi\n
    \n", "html": "\n\n\n\n
    \nHi\n
    \n", "example": 190, "start_line": 3103, "end_line": 3123, "section": "HTML blocks" }, { "markdown": "\n\n \n\n \n\n \n\n
    \n Hi\n
    \n", "html": "\n \n
    <td>\n  Hi\n</td>\n
    \n \n
    \n", "example": 191, "start_line": 3130, "end_line": 3151, "section": "HTML blocks" }, { "markdown": "[foo]: /url \"title\"\n\n[foo]\n", "html": "

    foo

    \n", "example": 192, "start_line": 3179, "end_line": 3185, "section": "Link reference definitions" }, { "markdown": " [foo]: \n /url \n 'the title' \n\n[foo]\n", "html": "

    foo

    \n", "example": 193, "start_line": 3188, "end_line": 3196, "section": "Link reference definitions" }, { "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n", "html": "

    Foo*bar]

    \n", "example": 194, "start_line": 3199, "end_line": 3205, "section": "Link reference definitions" }, { "markdown": "[Foo bar]:\n\n'title'\n\n[Foo bar]\n", "html": "

    Foo bar

    \n", "example": 195, "start_line": 3208, "end_line": 3216, "section": "Link reference definitions" }, { "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n", "html": "

    foo

    \n", "example": 196, "start_line": 3221, "end_line": 3235, "section": "Link reference definitions" }, { "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n", "html": "

    [foo]: /url 'title

    \n

    with blank line'

    \n

    [foo]

    \n", "example": 197, "start_line": 3240, "end_line": 3250, "section": "Link reference definitions" }, { "markdown": "[foo]:\n/url\n\n[foo]\n", "html": "

    foo

    \n", "example": 198, "start_line": 3255, "end_line": 3262, "section": "Link reference definitions" }, { "markdown": "[foo]:\n\n[foo]\n", "html": "

    [foo]:

    \n

    [foo]

    \n", "example": 199, "start_line": 3267, "end_line": 3274, "section": "Link reference definitions" }, { "markdown": "[foo]: <>\n\n[foo]\n", "html": "

    foo

    \n", "example": 200, "start_line": 3279, "end_line": 3285, "section": "Link reference definitions" }, { "markdown": "[foo]: (baz)\n\n[foo]\n", "html": "

    [foo]: (baz)

    \n

    [foo]

    \n", "example": 201, "start_line": 3290, "end_line": 3297, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n", "html": "

    foo

    \n", "example": 202, "start_line": 3303, "end_line": 3309, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: url\n", "html": "

    foo

    \n", "example": 203, "start_line": 3314, "end_line": 3320, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", "html": "

    foo

    \n", "example": 204, "start_line": 3326, "end_line": 3333, "section": "Link reference definitions" }, { "markdown": "[FOO]: /url\n\n[Foo]\n", "html": "

    Foo

    \n", "example": 205, "start_line": 3339, "end_line": 3345, "section": "Link reference definitions" }, { "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", "html": "

    αγω

    \n", "example": 206, "start_line": 3348, "end_line": 3354, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", "example": 207, "start_line": 3363, "end_line": 3366, "section": "Link reference definitions" }, { "markdown": "[\nfoo\n]: /url\nbar\n", "html": "

    bar

    \n", "example": 208, "start_line": 3371, "end_line": 3378, "section": "Link reference definitions" }, { "markdown": "[foo]: /url \"title\" ok\n", "html": "

    [foo]: /url "title" ok

    \n", "example": 209, "start_line": 3384, "end_line": 3388, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n\"title\" ok\n", "html": "

    "title" ok

    \n", "example": 210, "start_line": 3393, "end_line": 3398, "section": "Link reference definitions" }, { "markdown": " [foo]: /url \"title\"\n\n[foo]\n", "html": "
    [foo]: /url "title"\n
    \n

    [foo]

    \n", "example": 211, "start_line": 3404, "end_line": 3412, "section": "Link reference definitions" }, { "markdown": "```\n[foo]: /url\n```\n\n[foo]\n", "html": "
    [foo]: /url\n
    \n

    [foo]

    \n", "example": 212, "start_line": 3418, "end_line": 3428, "section": "Link reference definitions" }, { "markdown": "Foo\n[bar]: /baz\n\n[bar]\n", "html": "

    Foo\n[bar]: /baz

    \n

    [bar]

    \n", "example": 213, "start_line": 3433, "end_line": 3442, "section": "Link reference definitions" }, { "markdown": "# [Foo]\n[foo]: /url\n> bar\n", "html": "

    Foo

    \n
    \n

    bar

    \n
    \n", "example": 214, "start_line": 3448, "end_line": 3457, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\nbar\n===\n[foo]\n", "html": "

    bar

    \n

    foo

    \n", "example": 215, "start_line": 3459, "end_line": 3467, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n===\n[foo]\n", "html": "

    ===\nfoo

    \n", "example": 216, "start_line": 3469, "end_line": 3476, "section": "Link reference definitions" }, { "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n", "html": "

    foo,\nbar,\nbaz

    \n", "example": 217, "start_line": 3482, "end_line": 3495, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n> [foo]: /url\n", "html": "

    foo

    \n
    \n
    \n", "example": 218, "start_line": 3503, "end_line": 3511, "section": "Link reference definitions" }, { "markdown": "aaa\n\nbbb\n", "html": "

    aaa

    \n

    bbb

    \n", "example": 219, "start_line": 3525, "end_line": 3532, "section": "Paragraphs" }, { "markdown": "aaa\nbbb\n\nccc\nddd\n", "html": "

    aaa\nbbb

    \n

    ccc\nddd

    \n", "example": 220, "start_line": 3537, "end_line": 3548, "section": "Paragraphs" }, { "markdown": "aaa\n\n\nbbb\n", "html": "

    aaa

    \n

    bbb

    \n", "example": 221, "start_line": 3553, "end_line": 3561, "section": "Paragraphs" }, { "markdown": " aaa\n bbb\n", "html": "

    aaa\nbbb

    \n", "example": 222, "start_line": 3566, "end_line": 3572, "section": "Paragraphs" }, { "markdown": "aaa\n bbb\n ccc\n", "html": "

    aaa\nbbb\nccc

    \n", "example": 223, "start_line": 3578, "end_line": 3586, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "

    aaa\nbbb

    \n", "example": 224, "start_line": 3592, "end_line": 3598, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "
    aaa\n
    \n

    bbb

    \n", "example": 225, "start_line": 3601, "end_line": 3608, "section": "Paragraphs" }, { "markdown": "aaa \nbbb \n", "html": "

    aaa
    \nbbb

    \n", "example": 226, "start_line": 3615, "end_line": 3621, "section": "Paragraphs" }, { "markdown": " \n\naaa\n \n\n# aaa\n\n \n", "html": "

    aaa

    \n

    aaa

    \n", "example": 227, "start_line": 3632, "end_line": 3644, "section": "Blank lines" }, { "markdown": "> # Foo\n> bar\n> baz\n", "html": "
    \n

    Foo

    \n

    bar\nbaz

    \n
    \n", "example": 228, "start_line": 3700, "end_line": 3710, "section": "Block quotes" }, { "markdown": "># Foo\n>bar\n> baz\n", "html": "
    \n

    Foo

    \n

    bar\nbaz

    \n
    \n", "example": 229, "start_line": 3715, "end_line": 3725, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
    \n

    Foo

    \n

    bar\nbaz

    \n
    \n", "example": 230, "start_line": 3730, "end_line": 3740, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
    > # Foo\n> bar\n> baz\n
    \n", "example": 231, "start_line": 3745, "end_line": 3754, "section": "Block quotes" }, { "markdown": "> # Foo\n> bar\nbaz\n", "html": "
    \n

    Foo

    \n

    bar\nbaz

    \n
    \n", "example": 232, "start_line": 3760, "end_line": 3770, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n> foo\n", "html": "
    \n

    bar\nbaz\nfoo

    \n
    \n", "example": 233, "start_line": 3776, "end_line": 3786, "section": "Block quotes" }, { "markdown": "> foo\n---\n", "html": "
    \n

    foo

    \n
    \n
    \n", "example": 234, "start_line": 3800, "end_line": 3808, "section": "Block quotes" }, { "markdown": "> - foo\n- bar\n", "html": "
    \n
      \n
    • foo
    • \n
    \n
    \n
      \n
    • bar
    • \n
    \n", "example": 235, "start_line": 3820, "end_line": 3832, "section": "Block quotes" }, { "markdown": "> foo\n bar\n", "html": "
    \n
    foo\n
    \n
    \n
    bar\n
    \n", "example": 236, "start_line": 3838, "end_line": 3848, "section": "Block quotes" }, { "markdown": "> ```\nfoo\n```\n", "html": "
    \n
    \n
    \n

    foo

    \n
    \n", "example": 237, "start_line": 3851, "end_line": 3861, "section": "Block quotes" }, { "markdown": "> foo\n - bar\n", "html": "
    \n

    foo\n- bar

    \n
    \n", "example": 238, "start_line": 3867, "end_line": 3875, "section": "Block quotes" }, { "markdown": ">\n", "html": "
    \n
    \n", "example": 239, "start_line": 3891, "end_line": 3896, "section": "Block quotes" }, { "markdown": ">\n> \n> \n", "html": "
    \n
    \n", "example": 240, "start_line": 3899, "end_line": 3906, "section": "Block quotes" }, { "markdown": ">\n> foo\n> \n", "html": "
    \n

    foo

    \n
    \n", "example": 241, "start_line": 3911, "end_line": 3919, "section": "Block quotes" }, { "markdown": "> foo\n\n> bar\n", "html": "
    \n

    foo

    \n
    \n
    \n

    bar

    \n
    \n", "example": 242, "start_line": 3924, "end_line": 3935, "section": "Block quotes" }, { "markdown": "> foo\n> bar\n", "html": "
    \n

    foo\nbar

    \n
    \n", "example": 243, "start_line": 3946, "end_line": 3954, "section": "Block quotes" }, { "markdown": "> foo\n>\n> bar\n", "html": "
    \n

    foo

    \n

    bar

    \n
    \n", "example": 244, "start_line": 3959, "end_line": 3968, "section": "Block quotes" }, { "markdown": "foo\n> bar\n", "html": "

    foo

    \n
    \n

    bar

    \n
    \n", "example": 245, "start_line": 3973, "end_line": 3981, "section": "Block quotes" }, { "markdown": "> aaa\n***\n> bbb\n", "html": "
    \n

    aaa

    \n
    \n
    \n
    \n

    bbb

    \n
    \n", "example": 246, "start_line": 3987, "end_line": 3999, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n", "html": "
    \n

    bar\nbaz

    \n
    \n", "example": 247, "start_line": 4005, "end_line": 4013, "section": "Block quotes" }, { "markdown": "> bar\n\nbaz\n", "html": "
    \n

    bar

    \n
    \n

    baz

    \n", "example": 248, "start_line": 4016, "end_line": 4025, "section": "Block quotes" }, { "markdown": "> bar\n>\nbaz\n", "html": "
    \n

    bar

    \n
    \n

    baz

    \n", "example": 249, "start_line": 4028, "end_line": 4037, "section": "Block quotes" }, { "markdown": "> > > foo\nbar\n", "html": "
    \n
    \n
    \n

    foo\nbar

    \n
    \n
    \n
    \n", "example": 250, "start_line": 4044, "end_line": 4056, "section": "Block quotes" }, { "markdown": ">>> foo\n> bar\n>>baz\n", "html": "
    \n
    \n
    \n

    foo\nbar\nbaz

    \n
    \n
    \n
    \n", "example": 251, "start_line": 4059, "end_line": 4073, "section": "Block quotes" }, { "markdown": "> code\n\n> not code\n", "html": "
    \n
    code\n
    \n
    \n
    \n

    not code

    \n
    \n", "example": 252, "start_line": 4081, "end_line": 4093, "section": "Block quotes" }, { "markdown": "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n", "html": "

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n", "example": 253, "start_line": 4135, "end_line": 4150, "section": "List items" }, { "markdown": "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
      \n
    1. \n

      A paragraph\nwith two lines.

      \n
      indented code\n
      \n
      \n

      A block quote.

      \n
      \n
    2. \n
    \n", "example": 254, "start_line": 4157, "end_line": 4176, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
      \n
    • one
    • \n
    \n

    two

    \n", "example": 255, "start_line": 4190, "end_line": 4199, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
      \n
    • \n

      one

      \n

      two

      \n
    • \n
    \n", "example": 256, "start_line": 4202, "end_line": 4213, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
      \n
    • one
    • \n
    \n
     two\n
    \n", "example": 257, "start_line": 4216, "end_line": 4226, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
      \n
    • \n

      one

      \n

      two

      \n
    • \n
    \n", "example": 258, "start_line": 4229, "end_line": 4240, "section": "List items" }, { "markdown": " > > 1. one\n>>\n>> two\n", "html": "
    \n
    \n
      \n
    1. \n

      one

      \n

      two

      \n
    2. \n
    \n
    \n
    \n", "example": 259, "start_line": 4251, "end_line": 4266, "section": "List items" }, { "markdown": ">>- one\n>>\n > > two\n", "html": "
    \n
    \n
      \n
    • one
    • \n
    \n

    two

    \n
    \n
    \n", "example": 260, "start_line": 4278, "end_line": 4291, "section": "List items" }, { "markdown": "-one\n\n2.two\n", "html": "

    -one

    \n

    2.two

    \n", "example": 261, "start_line": 4297, "end_line": 4304, "section": "List items" }, { "markdown": "- foo\n\n\n bar\n", "html": "
      \n
    • \n

      foo

      \n

      bar

      \n
    • \n
    \n", "example": 262, "start_line": 4310, "end_line": 4322, "section": "List items" }, { "markdown": "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n", "html": "
      \n
    1. \n

      foo

      \n
      bar\n
      \n

      baz

      \n
      \n

      bam

      \n
      \n
    2. \n
    \n", "example": 263, "start_line": 4327, "end_line": 4349, "section": "List items" }, { "markdown": "- Foo\n\n bar\n\n\n baz\n", "html": "
      \n
    • \n

      Foo

      \n
      bar\n\n\nbaz\n
      \n
    • \n
    \n", "example": 264, "start_line": 4355, "end_line": 4373, "section": "List items" }, { "markdown": "123456789. ok\n", "html": "
      \n
    1. ok
    2. \n
    \n", "example": 265, "start_line": 4377, "end_line": 4383, "section": "List items" }, { "markdown": "1234567890. not ok\n", "html": "

    1234567890. not ok

    \n", "example": 266, "start_line": 4386, "end_line": 4390, "section": "List items" }, { "markdown": "0. ok\n", "html": "
      \n
    1. ok
    2. \n
    \n", "example": 267, "start_line": 4395, "end_line": 4401, "section": "List items" }, { "markdown": "003. ok\n", "html": "
      \n
    1. ok
    2. \n
    \n", "example": 268, "start_line": 4404, "end_line": 4410, "section": "List items" }, { "markdown": "-1. not ok\n", "html": "

    -1. not ok

    \n", "example": 269, "start_line": 4415, "end_line": 4419, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
      \n
    • \n

      foo

      \n
      bar\n
      \n
    • \n
    \n", "example": 270, "start_line": 4438, "end_line": 4450, "section": "List items" }, { "markdown": " 10. foo\n\n bar\n", "html": "
      \n
    1. \n

      foo

      \n
      bar\n
      \n
    2. \n
    \n", "example": 271, "start_line": 4455, "end_line": 4467, "section": "List items" }, { "markdown": " indented code\n\nparagraph\n\n more code\n", "html": "
    indented code\n
    \n

    paragraph

    \n
    more code\n
    \n", "example": 272, "start_line": 4474, "end_line": 4486, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
      \n
    1. \n
      indented code\n
      \n

      paragraph

      \n
      more code\n
      \n
    2. \n
    \n", "example": 273, "start_line": 4489, "end_line": 4505, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
      \n
    1. \n
       indented code\n
      \n

      paragraph

      \n
      more code\n
      \n
    2. \n
    \n", "example": 274, "start_line": 4511, "end_line": 4527, "section": "List items" }, { "markdown": " foo\n\nbar\n", "html": "

    foo

    \n

    bar

    \n", "example": 275, "start_line": 4538, "end_line": 4545, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
      \n
    • foo
    • \n
    \n

    bar

    \n", "example": 276, "start_line": 4548, "end_line": 4557, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
      \n
    • \n

      foo

      \n

      bar

      \n
    • \n
    \n", "example": 277, "start_line": 4565, "end_line": 4576, "section": "List items" }, { "markdown": "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n", "html": "
      \n
    • foo
    • \n
    • \n
      bar\n
      \n
    • \n
    • \n
      baz\n
      \n
    • \n
    \n", "example": 278, "start_line": 4592, "end_line": 4613, "section": "List items" }, { "markdown": "- \n foo\n", "html": "
      \n
    • foo
    • \n
    \n", "example": 279, "start_line": 4618, "end_line": 4625, "section": "List items" }, { "markdown": "-\n\n foo\n", "html": "
      \n
    • \n
    \n

    foo

    \n", "example": 280, "start_line": 4632, "end_line": 4641, "section": "List items" }, { "markdown": "- foo\n-\n- bar\n", "html": "
      \n
    • foo
    • \n
    • \n
    • bar
    • \n
    \n", "example": 281, "start_line": 4646, "end_line": 4656, "section": "List items" }, { "markdown": "- foo\n- \n- bar\n", "html": "
      \n
    • foo
    • \n
    • \n
    • bar
    • \n
    \n", "example": 282, "start_line": 4661, "end_line": 4671, "section": "List items" }, { "markdown": "1. foo\n2.\n3. bar\n", "html": "
      \n
    1. foo
    2. \n
    3. \n
    4. bar
    5. \n
    \n", "example": 283, "start_line": 4676, "end_line": 4686, "section": "List items" }, { "markdown": "*\n", "html": "
      \n
    • \n
    \n", "example": 284, "start_line": 4691, "end_line": 4697, "section": "List items" }, { "markdown": "foo\n*\n\nfoo\n1.\n", "html": "

    foo\n*

    \n

    foo\n1.

    \n", "example": 285, "start_line": 4701, "end_line": 4712, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
      \n
    1. \n

      A paragraph\nwith two lines.

      \n
      indented code\n
      \n
      \n

      A block quote.

      \n
      \n
    2. \n
    \n", "example": 286, "start_line": 4723, "end_line": 4742, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
      \n
    1. \n

      A paragraph\nwith two lines.

      \n
      indented code\n
      \n
      \n

      A block quote.

      \n
      \n
    2. \n
    \n", "example": 287, "start_line": 4747, "end_line": 4766, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
      \n
    1. \n

      A paragraph\nwith two lines.

      \n
      indented code\n
      \n
      \n

      A block quote.

      \n
      \n
    2. \n
    \n", "example": 288, "start_line": 4771, "end_line": 4790, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n
    \n", "example": 289, "start_line": 4795, "end_line": 4810, "section": "List items" }, { "markdown": " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n", "html": "
      \n
    1. \n

      A paragraph\nwith two lines.

      \n
      indented code\n
      \n
      \n

      A block quote.

      \n
      \n
    2. \n
    \n", "example": 290, "start_line": 4825, "end_line": 4844, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n", "html": "
      \n
    1. A paragraph\nwith two lines.
    2. \n
    \n", "example": 291, "start_line": 4849, "end_line": 4857, "section": "List items" }, { "markdown": "> 1. > Blockquote\ncontinued here.\n", "html": "
    \n
      \n
    1. \n
      \n

      Blockquote\ncontinued here.

      \n
      \n
    2. \n
    \n
    \n", "example": 292, "start_line": 4862, "end_line": 4876, "section": "List items" }, { "markdown": "> 1. > Blockquote\n> continued here.\n", "html": "
    \n
      \n
    1. \n
      \n

      Blockquote\ncontinued here.

      \n
      \n
    2. \n
    \n
    \n", "example": 293, "start_line": 4879, "end_line": 4893, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
      \n
    • foo\n
        \n
      • bar\n
          \n
        • baz\n
            \n
          • boo
          • \n
          \n
        • \n
        \n
      • \n
      \n
    • \n
    \n", "example": 294, "start_line": 4907, "end_line": 4928, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
      \n
    • foo
    • \n
    • bar
    • \n
    • baz
    • \n
    • boo
    • \n
    \n", "example": 295, "start_line": 4933, "end_line": 4945, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
      \n
    1. foo\n
        \n
      • bar
      • \n
      \n
    2. \n
    \n", "example": 296, "start_line": 4950, "end_line": 4961, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
      \n
    1. foo
    2. \n
    \n
      \n
    • bar
    • \n
    \n", "example": 297, "start_line": 4966, "end_line": 4976, "section": "List items" }, { "markdown": "- - foo\n", "html": "
      \n
    • \n
        \n
      • foo
      • \n
      \n
    • \n
    \n", "example": 298, "start_line": 4981, "end_line": 4991, "section": "List items" }, { "markdown": "1. - 2. foo\n", "html": "
      \n
    1. \n
        \n
      • \n
          \n
        1. foo
        2. \n
        \n
      • \n
      \n
    2. \n
    \n", "example": 299, "start_line": 4994, "end_line": 5008, "section": "List items" }, { "markdown": "- # Foo\n- Bar\n ---\n baz\n", "html": "
      \n
    • \n

      Foo

      \n
    • \n
    • \n

      Bar

      \nbaz
    • \n
    \n", "example": 300, "start_line": 5013, "end_line": 5027, "section": "List items" }, { "markdown": "- foo\n- bar\n+ baz\n", "html": "
      \n
    • foo
    • \n
    • bar
    • \n
    \n
      \n
    • baz
    • \n
    \n", "example": 301, "start_line": 5249, "end_line": 5261, "section": "Lists" }, { "markdown": "1. foo\n2. bar\n3) baz\n", "html": "
      \n
    1. foo
    2. \n
    3. bar
    4. \n
    \n
      \n
    1. baz
    2. \n
    \n", "example": 302, "start_line": 5264, "end_line": 5276, "section": "Lists" }, { "markdown": "Foo\n- bar\n- baz\n", "html": "

    Foo

    \n
      \n
    • bar
    • \n
    • baz
    • \n
    \n", "example": 303, "start_line": 5283, "end_line": 5293, "section": "Lists" }, { "markdown": "The number of windows in my house is\n14. The number of doors is 6.\n", "html": "

    The number of windows in my house is\n14. The number of doors is 6.

    \n", "example": 304, "start_line": 5360, "end_line": 5366, "section": "Lists" }, { "markdown": "The number of windows in my house is\n1. The number of doors is 6.\n", "html": "

    The number of windows in my house is

    \n
      \n
    1. The number of doors is 6.
    2. \n
    \n", "example": 305, "start_line": 5370, "end_line": 5378, "section": "Lists" }, { "markdown": "- foo\n\n- bar\n\n\n- baz\n", "html": "
      \n
    • \n

      foo

      \n
    • \n
    • \n

      bar

      \n
    • \n
    • \n

      baz

      \n
    • \n
    \n", "example": 306, "start_line": 5384, "end_line": 5403, "section": "Lists" }, { "markdown": "- foo\n - bar\n - baz\n\n\n bim\n", "html": "
      \n
    • foo\n
        \n
      • bar\n
          \n
        • \n

          baz

          \n

          bim

          \n
        • \n
        \n
      • \n
      \n
    • \n
    \n", "example": 307, "start_line": 5405, "end_line": 5427, "section": "Lists" }, { "markdown": "- foo\n- bar\n\n\n\n- baz\n- bim\n", "html": "
      \n
    • foo
    • \n
    • bar
    • \n
    \n\n
      \n
    • baz
    • \n
    • bim
    • \n
    \n", "example": 308, "start_line": 5435, "end_line": 5453, "section": "Lists" }, { "markdown": "- foo\n\n notcode\n\n- foo\n\n\n\n code\n", "html": "
      \n
    • \n

      foo

      \n

      notcode

      \n
    • \n
    • \n

      foo

      \n
    • \n
    \n\n
    code\n
    \n", "example": 309, "start_line": 5456, "end_line": 5479, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n - f\n- g\n", "html": "
      \n
    • a
    • \n
    • b
    • \n
    • c
    • \n
    • d
    • \n
    • e
    • \n
    • f
    • \n
    • g
    • \n
    \n", "example": 310, "start_line": 5487, "end_line": 5505, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
      \n
    1. \n

      a

      \n
    2. \n
    3. \n

      b

      \n
    4. \n
    5. \n

      c

      \n
    6. \n
    \n", "example": 311, "start_line": 5508, "end_line": 5526, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n", "html": "
      \n
    • a
    • \n
    • b
    • \n
    • c
    • \n
    • d\n- e
    • \n
    \n", "example": 312, "start_line": 5532, "end_line": 5546, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
      \n
    1. \n

      a

      \n
    2. \n
    3. \n

      b

      \n
    4. \n
    \n
    3. c\n
    \n", "example": 313, "start_line": 5552, "end_line": 5569, "section": "Lists" }, { "markdown": "- a\n- b\n\n- c\n", "html": "
      \n
    • \n

      a

      \n
    • \n
    • \n

      b

      \n
    • \n
    • \n

      c

      \n
    • \n
    \n", "example": 314, "start_line": 5575, "end_line": 5592, "section": "Lists" }, { "markdown": "* a\n*\n\n* c\n", "html": "
      \n
    • \n

      a

      \n
    • \n
    • \n
    • \n

      c

      \n
    • \n
    \n", "example": 315, "start_line": 5597, "end_line": 5612, "section": "Lists" }, { "markdown": "- a\n- b\n\n c\n- d\n", "html": "
      \n
    • \n

      a

      \n
    • \n
    • \n

      b

      \n

      c

      \n
    • \n
    • \n

      d

      \n
    • \n
    \n", "example": 316, "start_line": 5619, "end_line": 5638, "section": "Lists" }, { "markdown": "- a\n- b\n\n [ref]: /url\n- d\n", "html": "
      \n
    • \n

      a

      \n
    • \n
    • \n

      b

      \n
    • \n
    • \n

      d

      \n
    • \n
    \n", "example": 317, "start_line": 5641, "end_line": 5659, "section": "Lists" }, { "markdown": "- a\n- ```\n b\n\n\n ```\n- c\n", "html": "
      \n
    • a
    • \n
    • \n
      b\n\n\n
      \n
    • \n
    • c
    • \n
    \n", "example": 318, "start_line": 5664, "end_line": 5683, "section": "Lists" }, { "markdown": "- a\n - b\n\n c\n- d\n", "html": "
      \n
    • a\n
        \n
      • \n

        b

        \n

        c

        \n
      • \n
      \n
    • \n
    • d
    • \n
    \n", "example": 319, "start_line": 5690, "end_line": 5708, "section": "Lists" }, { "markdown": "* a\n > b\n >\n* c\n", "html": "
      \n
    • a\n
      \n

      b

      \n
      \n
    • \n
    • c
    • \n
    \n", "example": 320, "start_line": 5714, "end_line": 5728, "section": "Lists" }, { "markdown": "- a\n > b\n ```\n c\n ```\n- d\n", "html": "
      \n
    • a\n
      \n

      b

      \n
      \n
      c\n
      \n
    • \n
    • d
    • \n
    \n", "example": 321, "start_line": 5734, "end_line": 5752, "section": "Lists" }, { "markdown": "- a\n", "html": "
      \n
    • a
    • \n
    \n", "example": 322, "start_line": 5757, "end_line": 5763, "section": "Lists" }, { "markdown": "- a\n - b\n", "html": "
      \n
    • a\n
        \n
      • b
      • \n
      \n
    • \n
    \n", "example": 323, "start_line": 5766, "end_line": 5777, "section": "Lists" }, { "markdown": "1. ```\n foo\n ```\n\n bar\n", "html": "
      \n
    1. \n
      foo\n
      \n

      bar

      \n
    2. \n
    \n", "example": 324, "start_line": 5783, "end_line": 5797, "section": "Lists" }, { "markdown": "* foo\n * bar\n\n baz\n", "html": "
      \n
    • \n

      foo

      \n
        \n
      • bar
      • \n
      \n

      baz

      \n
    • \n
    \n", "example": 325, "start_line": 5802, "end_line": 5817, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n\n- d\n - e\n - f\n", "html": "
      \n
    • \n

      a

      \n
        \n
      • b
      • \n
      • c
      • \n
      \n
    • \n
    • \n

      d

      \n
        \n
      • e
      • \n
      • f
      • \n
      \n
    • \n
    \n", "example": 326, "start_line": 5820, "end_line": 5845, "section": "Lists" }, { "markdown": "`hi`lo`\n", "html": "

    hilo`

    \n", "example": 327, "start_line": 5854, "end_line": 5858, "section": "Inlines" }, { "markdown": "`foo`\n", "html": "

    foo

    \n", "example": 328, "start_line": 5886, "end_line": 5890, "section": "Code spans" }, { "markdown": "`` foo ` bar ``\n", "html": "

    foo ` bar

    \n", "example": 329, "start_line": 5897, "end_line": 5901, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

    ``

    \n", "example": 330, "start_line": 5907, "end_line": 5911, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

    ``

    \n", "example": 331, "start_line": 5915, "end_line": 5919, "section": "Code spans" }, { "markdown": "` a`\n", "html": "

    a

    \n", "example": 332, "start_line": 5924, "end_line": 5928, "section": "Code spans" }, { "markdown": "` b `\n", "html": "

     b 

    \n", "example": 333, "start_line": 5933, "end_line": 5937, "section": "Code spans" }, { "markdown": "` `\n` `\n", "html": "

     \n

    \n", "example": 334, "start_line": 5941, "end_line": 5947, "section": "Code spans" }, { "markdown": "``\nfoo\nbar \nbaz\n``\n", "html": "

    foo bar baz

    \n", "example": 335, "start_line": 5952, "end_line": 5960, "section": "Code spans" }, { "markdown": "``\nfoo \n``\n", "html": "

    foo

    \n", "example": 336, "start_line": 5962, "end_line": 5968, "section": "Code spans" }, { "markdown": "`foo bar \nbaz`\n", "html": "

    foo bar baz

    \n", "example": 337, "start_line": 5973, "end_line": 5978, "section": "Code spans" }, { "markdown": "`foo\\`bar`\n", "html": "

    foo\\bar`

    \n", "example": 338, "start_line": 5990, "end_line": 5994, "section": "Code spans" }, { "markdown": "``foo`bar``\n", "html": "

    foo`bar

    \n", "example": 339, "start_line": 6001, "end_line": 6005, "section": "Code spans" }, { "markdown": "` foo `` bar `\n", "html": "

    foo `` bar

    \n", "example": 340, "start_line": 6007, "end_line": 6011, "section": "Code spans" }, { "markdown": "*foo`*`\n", "html": "

    *foo*

    \n", "example": 341, "start_line": 6019, "end_line": 6023, "section": "Code spans" }, { "markdown": "[not a `link](/foo`)\n", "html": "

    [not a link](/foo)

    \n", "example": 342, "start_line": 6028, "end_line": 6032, "section": "Code spans" }, { "markdown": "``\n", "html": "

    <a href="">`

    \n", "example": 343, "start_line": 6038, "end_line": 6042, "section": "Code spans" }, { "markdown": "
    `\n", "html": "

    `

    \n", "example": 344, "start_line": 6047, "end_line": 6051, "section": "Code spans" }, { "markdown": "``\n", "html": "

    <https://foo.bar.baz>`

    \n", "example": 345, "start_line": 6056, "end_line": 6060, "section": "Code spans" }, { "markdown": "`\n", "html": "

    https://foo.bar.`baz`

    \n", "example": 346, "start_line": 6065, "end_line": 6069, "section": "Code spans" }, { "markdown": "```foo``\n", "html": "

    ```foo``

    \n", "example": 347, "start_line": 6075, "end_line": 6079, "section": "Code spans" }, { "markdown": "`foo\n", "html": "

    `foo

    \n", "example": 348, "start_line": 6082, "end_line": 6086, "section": "Code spans" }, { "markdown": "`foo``bar``\n", "html": "

    `foobar

    \n", "example": 349, "start_line": 6091, "end_line": 6095, "section": "Code spans" }, { "markdown": "*foo bar*\n", "html": "

    foo bar

    \n", "example": 350, "start_line": 6308, "end_line": 6312, "section": "Emphasis and strong emphasis" }, { "markdown": "a * foo bar*\n", "html": "

    a * foo bar*

    \n", "example": 351, "start_line": 6318, "end_line": 6322, "section": "Emphasis and strong emphasis" }, { "markdown": "a*\"foo\"*\n", "html": "

    a*"foo"*

    \n", "example": 352, "start_line": 6329, "end_line": 6333, "section": "Emphasis and strong emphasis" }, { "markdown": "* a *\n", "html": "

    * a *

    \n", "example": 353, "start_line": 6338, "end_line": 6342, "section": "Emphasis and strong emphasis" }, { "markdown": "*$*alpha.\n\n*£*bravo.\n\n*€*charlie.\n", "html": "

    *$*alpha.

    \n

    *£*bravo.

    \n

    *€*charlie.

    \n", "example": 354, "start_line": 6347, "end_line": 6357, "section": "Emphasis and strong emphasis" }, { "markdown": "foo*bar*\n", "html": "

    foobar

    \n", "example": 355, "start_line": 6362, "end_line": 6366, "section": "Emphasis and strong emphasis" }, { "markdown": "5*6*78\n", "html": "

    5678

    \n", "example": 356, "start_line": 6369, "end_line": 6373, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar_\n", "html": "

    foo bar

    \n", "example": 357, "start_line": 6378, "end_line": 6382, "section": "Emphasis and strong emphasis" }, { "markdown": "_ foo bar_\n", "html": "

    _ foo bar_

    \n", "example": 358, "start_line": 6388, "end_line": 6392, "section": "Emphasis and strong emphasis" }, { "markdown": "a_\"foo\"_\n", "html": "

    a_"foo"_

    \n", "example": 359, "start_line": 6398, "end_line": 6402, "section": "Emphasis and strong emphasis" }, { "markdown": "foo_bar_\n", "html": "

    foo_bar_

    \n", "example": 360, "start_line": 6407, "end_line": 6411, "section": "Emphasis and strong emphasis" }, { "markdown": "5_6_78\n", "html": "

    5_6_78

    \n", "example": 361, "start_line": 6414, "end_line": 6418, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням_стремятся_\n", "html": "

    пристаням_стремятся_

    \n", "example": 362, "start_line": 6421, "end_line": 6425, "section": "Emphasis and strong emphasis" }, { "markdown": "aa_\"bb\"_cc\n", "html": "

    aa_"bb"_cc

    \n", "example": 363, "start_line": 6431, "end_line": 6435, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-_(bar)_\n", "html": "

    foo-(bar)

    \n", "example": 364, "start_line": 6442, "end_line": 6446, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo*\n", "html": "

    _foo*

    \n", "example": 365, "start_line": 6454, "end_line": 6458, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar *\n", "html": "

    *foo bar *

    \n", "example": 366, "start_line": 6464, "end_line": 6468, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar\n*\n", "html": "

    *foo bar\n*

    \n", "example": 367, "start_line": 6473, "end_line": 6479, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo)\n", "html": "

    *(*foo)

    \n", "example": 368, "start_line": 6486, "end_line": 6490, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo*)*\n", "html": "

    (foo)

    \n", "example": 369, "start_line": 6496, "end_line": 6500, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo*bar\n", "html": "

    foobar

    \n", "example": 370, "start_line": 6505, "end_line": 6509, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar _\n", "html": "

    _foo bar _

    \n", "example": 371, "start_line": 6518, "end_line": 6522, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo)\n", "html": "

    _(_foo)

    \n", "example": 372, "start_line": 6528, "end_line": 6532, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo_)_\n", "html": "

    (foo)

    \n", "example": 373, "start_line": 6537, "end_line": 6541, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar\n", "html": "

    _foo_bar

    \n", "example": 374, "start_line": 6546, "end_line": 6550, "section": "Emphasis and strong emphasis" }, { "markdown": "_пристаням_стремятся\n", "html": "

    _пристаням_стремятся

    \n", "example": 375, "start_line": 6553, "end_line": 6557, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar_baz_\n", "html": "

    foo_bar_baz

    \n", "example": 376, "start_line": 6560, "end_line": 6564, "section": "Emphasis and strong emphasis" }, { "markdown": "_(bar)_.\n", "html": "

    (bar).

    \n", "example": 377, "start_line": 6571, "end_line": 6575, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar**\n", "html": "

    foo bar

    \n", "example": 378, "start_line": 6580, "end_line": 6584, "section": "Emphasis and strong emphasis" }, { "markdown": "** foo bar**\n", "html": "

    ** foo bar**

    \n", "example": 379, "start_line": 6590, "end_line": 6594, "section": "Emphasis and strong emphasis" }, { "markdown": "a**\"foo\"**\n", "html": "

    a**"foo"**

    \n", "example": 380, "start_line": 6601, "end_line": 6605, "section": "Emphasis and strong emphasis" }, { "markdown": "foo**bar**\n", "html": "

    foobar

    \n", "example": 381, "start_line": 6610, "end_line": 6614, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar__\n", "html": "

    foo bar

    \n", "example": 382, "start_line": 6619, "end_line": 6623, "section": "Emphasis and strong emphasis" }, { "markdown": "__ foo bar__\n", "html": "

    __ foo bar__

    \n", "example": 383, "start_line": 6629, "end_line": 6633, "section": "Emphasis and strong emphasis" }, { "markdown": "__\nfoo bar__\n", "html": "

    __\nfoo bar__

    \n", "example": 384, "start_line": 6637, "end_line": 6643, "section": "Emphasis and strong emphasis" }, { "markdown": "a__\"foo\"__\n", "html": "

    a__"foo"__

    \n", "example": 385, "start_line": 6649, "end_line": 6653, "section": "Emphasis and strong emphasis" }, { "markdown": "foo__bar__\n", "html": "

    foo__bar__

    \n", "example": 386, "start_line": 6658, "end_line": 6662, "section": "Emphasis and strong emphasis" }, { "markdown": "5__6__78\n", "html": "

    5__6__78

    \n", "example": 387, "start_line": 6665, "end_line": 6669, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням__стремятся__\n", "html": "

    пристаням__стремятся__

    \n", "example": 388, "start_line": 6672, "end_line": 6676, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo, __bar__, baz__\n", "html": "

    foo, bar, baz

    \n", "example": 389, "start_line": 6679, "end_line": 6683, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-__(bar)__\n", "html": "

    foo-(bar)

    \n", "example": 390, "start_line": 6690, "end_line": 6694, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar **\n", "html": "

    **foo bar **

    \n", "example": 391, "start_line": 6703, "end_line": 6707, "section": "Emphasis and strong emphasis" }, { "markdown": "**(**foo)\n", "html": "

    **(**foo)

    \n", "example": 392, "start_line": 6716, "end_line": 6720, "section": "Emphasis and strong emphasis" }, { "markdown": "*(**foo**)*\n", "html": "

    (foo)

    \n", "example": 393, "start_line": 6726, "end_line": 6730, "section": "Emphasis and strong emphasis" }, { "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n", "html": "

    Gomphocarpus (Gomphocarpus physocarpus, syn.\nAsclepias physocarpa)

    \n", "example": 394, "start_line": 6733, "end_line": 6739, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo \"*bar*\" foo**\n", "html": "

    foo "bar" foo

    \n", "example": 395, "start_line": 6742, "end_line": 6746, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**bar\n", "html": "

    foobar

    \n", "example": 396, "start_line": 6751, "end_line": 6755, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar __\n", "html": "

    __foo bar __

    \n", "example": 397, "start_line": 6763, "end_line": 6767, "section": "Emphasis and strong emphasis" }, { "markdown": "__(__foo)\n", "html": "

    __(__foo)

    \n", "example": 398, "start_line": 6773, "end_line": 6777, "section": "Emphasis and strong emphasis" }, { "markdown": "_(__foo__)_\n", "html": "

    (foo)

    \n", "example": 399, "start_line": 6783, "end_line": 6787, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar\n", "html": "

    __foo__bar

    \n", "example": 400, "start_line": 6792, "end_line": 6796, "section": "Emphasis and strong emphasis" }, { "markdown": "__пристаням__стремятся\n", "html": "

    __пристаням__стремятся

    \n", "example": 401, "start_line": 6799, "end_line": 6803, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar__baz__\n", "html": "

    foo__bar__baz

    \n", "example": 402, "start_line": 6806, "end_line": 6810, "section": "Emphasis and strong emphasis" }, { "markdown": "__(bar)__.\n", "html": "

    (bar).

    \n", "example": 403, "start_line": 6817, "end_line": 6821, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [bar](/url)*\n", "html": "

    foo bar

    \n", "example": 404, "start_line": 6829, "end_line": 6833, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo\nbar*\n", "html": "

    foo\nbar

    \n", "example": 405, "start_line": 6836, "end_line": 6842, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo __bar__ baz_\n", "html": "

    foo bar baz

    \n", "example": 406, "start_line": 6848, "end_line": 6852, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo _bar_ baz_\n", "html": "

    foo bar baz

    \n", "example": 407, "start_line": 6855, "end_line": 6859, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_ bar_\n", "html": "

    foo bar

    \n", "example": 408, "start_line": 6862, "end_line": 6866, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar**\n", "html": "

    foo bar

    \n", "example": 409, "start_line": 6869, "end_line": 6873, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar** baz*\n", "html": "

    foo bar baz

    \n", "example": 410, "start_line": 6876, "end_line": 6880, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar**baz*\n", "html": "

    foobarbaz

    \n", "example": 411, "start_line": 6882, "end_line": 6886, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar*\n", "html": "

    foo**bar

    \n", "example": 412, "start_line": 6906, "end_line": 6910, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo** bar*\n", "html": "

    foo bar

    \n", "example": 413, "start_line": 6919, "end_line": 6923, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar***\n", "html": "

    foo bar

    \n", "example": 414, "start_line": 6926, "end_line": 6930, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar***\n", "html": "

    foobar

    \n", "example": 415, "start_line": 6933, "end_line": 6937, "section": "Emphasis and strong emphasis" }, { "markdown": "foo***bar***baz\n", "html": "

    foobarbaz

    \n", "example": 416, "start_line": 6944, "end_line": 6948, "section": "Emphasis and strong emphasis" }, { "markdown": "foo******bar*********baz\n", "html": "

    foobar***baz

    \n", "example": 417, "start_line": 6950, "end_line": 6954, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar *baz* bim** bop*\n", "html": "

    foo bar baz bim bop

    \n", "example": 418, "start_line": 6959, "end_line": 6963, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [*bar*](/url)*\n", "html": "

    foo bar

    \n", "example": 419, "start_line": 6966, "end_line": 6970, "section": "Emphasis and strong emphasis" }, { "markdown": "** is not an empty emphasis\n", "html": "

    ** is not an empty emphasis

    \n", "example": 420, "start_line": 6975, "end_line": 6979, "section": "Emphasis and strong emphasis" }, { "markdown": "**** is not an empty strong emphasis\n", "html": "

    **** is not an empty strong emphasis

    \n", "example": 421, "start_line": 6982, "end_line": 6986, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [bar](/url)**\n", "html": "

    foo bar

    \n", "example": 422, "start_line": 6995, "end_line": 6999, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo\nbar**\n", "html": "

    foo\nbar

    \n", "example": 423, "start_line": 7002, "end_line": 7008, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo _bar_ baz__\n", "html": "

    foo bar baz

    \n", "example": 424, "start_line": 7014, "end_line": 7018, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo __bar__ baz__\n", "html": "

    foo bar baz

    \n", "example": 425, "start_line": 7021, "end_line": 7025, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo__ bar__\n", "html": "

    foo bar

    \n", "example": 426, "start_line": 7028, "end_line": 7032, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar****\n", "html": "

    foo bar

    \n", "example": 427, "start_line": 7035, "end_line": 7039, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar* baz**\n", "html": "

    foo bar baz

    \n", "example": 428, "start_line": 7042, "end_line": 7046, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*bar*baz**\n", "html": "

    foobarbaz

    \n", "example": 429, "start_line": 7049, "end_line": 7053, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo* bar**\n", "html": "

    foo bar

    \n", "example": 430, "start_line": 7056, "end_line": 7060, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar***\n", "html": "

    foo bar

    \n", "example": 431, "start_line": 7063, "end_line": 7067, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar **baz**\nbim* bop**\n", "html": "

    foo bar baz\nbim bop

    \n", "example": 432, "start_line": 7072, "end_line": 7078, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [*bar*](/url)**\n", "html": "

    foo bar

    \n", "example": 433, "start_line": 7081, "end_line": 7085, "section": "Emphasis and strong emphasis" }, { "markdown": "__ is not an empty emphasis\n", "html": "

    __ is not an empty emphasis

    \n", "example": 434, "start_line": 7090, "end_line": 7094, "section": "Emphasis and strong emphasis" }, { "markdown": "____ is not an empty strong emphasis\n", "html": "

    ____ is not an empty strong emphasis

    \n", "example": 435, "start_line": 7097, "end_line": 7101, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ***\n", "html": "

    foo ***

    \n", "example": 436, "start_line": 7107, "end_line": 7111, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *\\**\n", "html": "

    foo *

    \n", "example": 437, "start_line": 7114, "end_line": 7118, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *_*\n", "html": "

    foo _

    \n", "example": 438, "start_line": 7121, "end_line": 7125, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *****\n", "html": "

    foo *****

    \n", "example": 439, "start_line": 7128, "end_line": 7132, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **\\***\n", "html": "

    foo *

    \n", "example": 440, "start_line": 7135, "end_line": 7139, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **_**\n", "html": "

    foo _

    \n", "example": 441, "start_line": 7142, "end_line": 7146, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*\n", "html": "

    *foo

    \n", "example": 442, "start_line": 7153, "end_line": 7157, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**\n", "html": "

    foo*

    \n", "example": 443, "start_line": 7160, "end_line": 7164, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo**\n", "html": "

    *foo

    \n", "example": 444, "start_line": 7167, "end_line": 7171, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo*\n", "html": "

    ***foo

    \n", "example": 445, "start_line": 7174, "end_line": 7178, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo***\n", "html": "

    foo*

    \n", "example": 446, "start_line": 7181, "end_line": 7185, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo****\n", "html": "

    foo***

    \n", "example": 447, "start_line": 7188, "end_line": 7192, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ___\n", "html": "

    foo ___

    \n", "example": 448, "start_line": 7198, "end_line": 7202, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _\\__\n", "html": "

    foo _

    \n", "example": 449, "start_line": 7205, "end_line": 7209, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _*_\n", "html": "

    foo *

    \n", "example": 450, "start_line": 7212, "end_line": 7216, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _____\n", "html": "

    foo _____

    \n", "example": 451, "start_line": 7219, "end_line": 7223, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __\\___\n", "html": "

    foo _

    \n", "example": 452, "start_line": 7226, "end_line": 7230, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __*__\n", "html": "

    foo *

    \n", "example": 453, "start_line": 7233, "end_line": 7237, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_\n", "html": "

    _foo

    \n", "example": 454, "start_line": 7240, "end_line": 7244, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo__\n", "html": "

    foo_

    \n", "example": 455, "start_line": 7251, "end_line": 7255, "section": "Emphasis and strong emphasis" }, { "markdown": "___foo__\n", "html": "

    _foo

    \n", "example": 456, "start_line": 7258, "end_line": 7262, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo_\n", "html": "

    ___foo

    \n", "example": 457, "start_line": 7265, "end_line": 7269, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo___\n", "html": "

    foo_

    \n", "example": 458, "start_line": 7272, "end_line": 7276, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo____\n", "html": "

    foo___

    \n", "example": 459, "start_line": 7279, "end_line": 7283, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**\n", "html": "

    foo

    \n", "example": 460, "start_line": 7289, "end_line": 7293, "section": "Emphasis and strong emphasis" }, { "markdown": "*_foo_*\n", "html": "

    foo

    \n", "example": 461, "start_line": 7296, "end_line": 7300, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__\n", "html": "

    foo

    \n", "example": 462, "start_line": 7303, "end_line": 7307, "section": "Emphasis and strong emphasis" }, { "markdown": "_*foo*_\n", "html": "

    foo

    \n", "example": 463, "start_line": 7310, "end_line": 7314, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo****\n", "html": "

    foo

    \n", "example": 464, "start_line": 7320, "end_line": 7324, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo____\n", "html": "

    foo

    \n", "example": 465, "start_line": 7327, "end_line": 7331, "section": "Emphasis and strong emphasis" }, { "markdown": "******foo******\n", "html": "

    foo

    \n", "example": 466, "start_line": 7338, "end_line": 7342, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo***\n", "html": "

    foo

    \n", "example": 467, "start_line": 7347, "end_line": 7351, "section": "Emphasis and strong emphasis" }, { "markdown": "_____foo_____\n", "html": "

    foo

    \n", "example": 468, "start_line": 7354, "end_line": 7358, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo _bar* baz_\n", "html": "

    foo _bar baz_

    \n", "example": 469, "start_line": 7363, "end_line": 7367, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo __bar *baz bim__ bam*\n", "html": "

    foo bar *baz bim bam

    \n", "example": 470, "start_line": 7370, "end_line": 7374, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar baz**\n", "html": "

    **foo bar baz

    \n", "example": 471, "start_line": 7379, "end_line": 7383, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar baz*\n", "html": "

    *foo bar baz

    \n", "example": 472, "start_line": 7386, "end_line": 7390, "section": "Emphasis and strong emphasis" }, { "markdown": "*[bar*](/url)\n", "html": "

    *bar*

    \n", "example": 473, "start_line": 7395, "end_line": 7399, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo [bar_](/url)\n", "html": "

    _foo bar_

    \n", "example": 474, "start_line": 7402, "end_line": 7406, "section": "Emphasis and strong emphasis" }, { "markdown": "*\n", "html": "

    *

    \n", "example": 475, "start_line": 7409, "end_line": 7413, "section": "Emphasis and strong emphasis" }, { "markdown": "**\n", "html": "

    **

    \n", "example": 476, "start_line": 7416, "end_line": 7420, "section": "Emphasis and strong emphasis" }, { "markdown": "__\n", "html": "

    __

    \n", "example": 477, "start_line": 7423, "end_line": 7427, "section": "Emphasis and strong emphasis" }, { "markdown": "*a `*`*\n", "html": "

    a *

    \n", "example": 478, "start_line": 7430, "end_line": 7434, "section": "Emphasis and strong emphasis" }, { "markdown": "_a `_`_\n", "html": "

    a _

    \n", "example": 479, "start_line": 7437, "end_line": 7441, "section": "Emphasis and strong emphasis" }, { "markdown": "**a\n", "html": "

    **ahttps://foo.bar/?q=**

    \n", "example": 480, "start_line": 7444, "end_line": 7448, "section": "Emphasis and strong emphasis" }, { "markdown": "__a\n", "html": "

    __ahttps://foo.bar/?q=__

    \n", "example": 481, "start_line": 7451, "end_line": 7455, "section": "Emphasis and strong emphasis" }, { "markdown": "[link](/uri \"title\")\n", "html": "

    link

    \n", "example": 482, "start_line": 7539, "end_line": 7543, "section": "Links" }, { "markdown": "[link](/uri)\n", "html": "

    link

    \n", "example": 483, "start_line": 7549, "end_line": 7553, "section": "Links" }, { "markdown": "[](./target.md)\n", "html": "

    \n", "example": 484, "start_line": 7555, "end_line": 7559, "section": "Links" }, { "markdown": "[link]()\n", "html": "

    link

    \n", "example": 485, "start_line": 7562, "end_line": 7566, "section": "Links" }, { "markdown": "[link](<>)\n", "html": "

    link

    \n", "example": 486, "start_line": 7569, "end_line": 7573, "section": "Links" }, { "markdown": "[]()\n", "html": "

    \n", "example": 487, "start_line": 7576, "end_line": 7580, "section": "Links" }, { "markdown": "[link](/my uri)\n", "html": "

    [link](/my uri)

    \n", "example": 488, "start_line": 7585, "end_line": 7589, "section": "Links" }, { "markdown": "[link](
    )\n", "html": "

    link

    \n", "example": 489, "start_line": 7591, "end_line": 7595, "section": "Links" }, { "markdown": "[link](foo\nbar)\n", "html": "

    [link](foo\nbar)

    \n", "example": 490, "start_line": 7600, "end_line": 7606, "section": "Links" }, { "markdown": "[link]()\n", "html": "

    [link]()

    \n", "example": 491, "start_line": 7608, "end_line": 7614, "section": "Links" }, { "markdown": "[a]()\n", "html": "

    a

    \n", "example": 492, "start_line": 7619, "end_line": 7623, "section": "Links" }, { "markdown": "[link]()\n", "html": "

    [link](<foo>)

    \n", "example": 493, "start_line": 7627, "end_line": 7631, "section": "Links" }, { "markdown": "[a](\n[a](c)\n", "html": "

    [a](<b)c\n[a](<b)c>\n[a](c)

    \n", "example": 494, "start_line": 7636, "end_line": 7644, "section": "Links" }, { "markdown": "[link](\\(foo\\))\n", "html": "

    link

    \n", "example": 495, "start_line": 7648, "end_line": 7652, "section": "Links" }, { "markdown": "[link](foo(and(bar)))\n", "html": "

    link

    \n", "example": 496, "start_line": 7657, "end_line": 7661, "section": "Links" }, { "markdown": "[link](foo(and(bar))\n", "html": "

    [link](foo(and(bar))

    \n", "example": 497, "start_line": 7666, "end_line": 7670, "section": "Links" }, { "markdown": "[link](foo\\(and\\(bar\\))\n", "html": "

    link

    \n", "example": 498, "start_line": 7673, "end_line": 7677, "section": "Links" }, { "markdown": "[link]()\n", "html": "

    link

    \n", "example": 499, "start_line": 7680, "end_line": 7684, "section": "Links" }, { "markdown": "[link](foo\\)\\:)\n", "html": "

    link

    \n", "example": 500, "start_line": 7690, "end_line": 7694, "section": "Links" }, { "markdown": "[link](#fragment)\n\n[link](https://example.com#fragment)\n\n[link](https://example.com?foo=3#frag)\n", "html": "

    link

    \n

    link

    \n

    link

    \n", "example": 501, "start_line": 7699, "end_line": 7709, "section": "Links" }, { "markdown": "[link](foo\\bar)\n", "html": "

    link

    \n", "example": 502, "start_line": 7715, "end_line": 7719, "section": "Links" }, { "markdown": "[link](foo%20bä)\n", "html": "

    link

    \n", "example": 503, "start_line": 7731, "end_line": 7735, "section": "Links" }, { "markdown": "[link](\"title\")\n", "html": "

    link

    \n", "example": 504, "start_line": 7742, "end_line": 7746, "section": "Links" }, { "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n", "html": "

    link\nlink\nlink

    \n", "example": 505, "start_line": 7751, "end_line": 7759, "section": "Links" }, { "markdown": "[link](/url \"title \\\""\")\n", "html": "

    link

    \n", "example": 506, "start_line": 7765, "end_line": 7769, "section": "Links" }, { "markdown": "[link](/url \"title\")\n", "html": "

    link

    \n", "example": 507, "start_line": 7776, "end_line": 7780, "section": "Links" }, { "markdown": "[link](/url \"title \"and\" title\")\n", "html": "

    [link](/url "title "and" title")

    \n", "example": 508, "start_line": 7785, "end_line": 7789, "section": "Links" }, { "markdown": "[link](/url 'title \"and\" title')\n", "html": "

    link

    \n", "example": 509, "start_line": 7794, "end_line": 7798, "section": "Links" }, { "markdown": "[link]( /uri\n \"title\" )\n", "html": "

    link

    \n", "example": 510, "start_line": 7819, "end_line": 7824, "section": "Links" }, { "markdown": "[link] (/uri)\n", "html": "

    [link] (/uri)

    \n", "example": 511, "start_line": 7830, "end_line": 7834, "section": "Links" }, { "markdown": "[link [foo [bar]]](/uri)\n", "html": "

    link [foo [bar]]

    \n", "example": 512, "start_line": 7840, "end_line": 7844, "section": "Links" }, { "markdown": "[link] bar](/uri)\n", "html": "

    [link] bar](/uri)

    \n", "example": 513, "start_line": 7847, "end_line": 7851, "section": "Links" }, { "markdown": "[link [bar](/uri)\n", "html": "

    [link bar

    \n", "example": 514, "start_line": 7854, "end_line": 7858, "section": "Links" }, { "markdown": "[link \\[bar](/uri)\n", "html": "

    link [bar

    \n", "example": 515, "start_line": 7861, "end_line": 7865, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*](/uri)\n", "html": "

    link foo bar #

    \n", "example": 516, "start_line": 7870, "end_line": 7874, "section": "Links" }, { "markdown": "[![moon](moon.jpg)](/uri)\n", "html": "

    \"moon\"

    \n", "example": 517, "start_line": 7877, "end_line": 7881, "section": "Links" }, { "markdown": "[foo [bar](/uri)](/uri)\n", "html": "

    [foo bar](/uri)

    \n", "example": 518, "start_line": 7886, "end_line": 7890, "section": "Links" }, { "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n", "html": "

    [foo [bar baz](/uri)](/uri)

    \n", "example": 519, "start_line": 7893, "end_line": 7897, "section": "Links" }, { "markdown": "![[[foo](uri1)](uri2)](uri3)\n", "html": "

    \"[foo](uri2)\"

    \n", "example": 520, "start_line": 7900, "end_line": 7904, "section": "Links" }, { "markdown": "*[foo*](/uri)\n", "html": "

    *foo*

    \n", "example": 521, "start_line": 7910, "end_line": 7914, "section": "Links" }, { "markdown": "[foo *bar](baz*)\n", "html": "

    foo *bar

    \n", "example": 522, "start_line": 7917, "end_line": 7921, "section": "Links" }, { "markdown": "*foo [bar* baz]\n", "html": "

    foo [bar baz]

    \n", "example": 523, "start_line": 7927, "end_line": 7931, "section": "Links" }, { "markdown": "[foo \n", "html": "

    [foo

    \n", "example": 524, "start_line": 7937, "end_line": 7941, "section": "Links" }, { "markdown": "[foo`](/uri)`\n", "html": "

    [foo](/uri)

    \n", "example": 525, "start_line": 7944, "end_line": 7948, "section": "Links" }, { "markdown": "[foo\n", "html": "

    [foohttps://example.com/?search=](uri)

    \n", "example": 526, "start_line": 7951, "end_line": 7955, "section": "Links" }, { "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", "html": "

    foo

    \n", "example": 527, "start_line": 7989, "end_line": 7995, "section": "Links" }, { "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", "html": "

    link [foo [bar]]

    \n", "example": 528, "start_line": 8004, "end_line": 8010, "section": "Links" }, { "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", "html": "

    link [bar

    \n", "example": 529, "start_line": 8013, "end_line": 8019, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n", "html": "

    link foo bar #

    \n", "example": 530, "start_line": 8024, "end_line": 8030, "section": "Links" }, { "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n", "html": "

    \"moon\"

    \n", "example": 531, "start_line": 8033, "end_line": 8039, "section": "Links" }, { "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n", "html": "

    [foo bar]ref

    \n", "example": 532, "start_line": 8044, "end_line": 8050, "section": "Links" }, { "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n", "html": "

    [foo bar baz]ref

    \n", "example": 533, "start_line": 8053, "end_line": 8059, "section": "Links" }, { "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", "html": "

    *foo*

    \n", "example": 534, "start_line": 8068, "end_line": 8074, "section": "Links" }, { "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n", "html": "

    foo *bar*

    \n", "example": 535, "start_line": 8077, "end_line": 8083, "section": "Links" }, { "markdown": "[foo \n\n[ref]: /uri\n", "html": "

    [foo

    \n", "example": 536, "start_line": 8089, "end_line": 8095, "section": "Links" }, { "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", "html": "

    [foo][ref]

    \n", "example": 537, "start_line": 8098, "end_line": 8104, "section": "Links" }, { "markdown": "[foo\n\n[ref]: /uri\n", "html": "

    [foohttps://example.com/?search=][ref]

    \n", "example": 538, "start_line": 8107, "end_line": 8113, "section": "Links" }, { "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", "html": "

    foo

    \n", "example": 539, "start_line": 8118, "end_line": 8124, "section": "Links" }, { "markdown": "[ẞ]\n\n[SS]: /url\n", "html": "

    \n", "example": 540, "start_line": 8129, "end_line": 8135, "section": "Links" }, { "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", "html": "

    Baz

    \n", "example": 541, "start_line": 8141, "end_line": 8148, "section": "Links" }, { "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", "html": "

    [foo] bar

    \n", "example": 542, "start_line": 8154, "end_line": 8160, "section": "Links" }, { "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n", "html": "

    [foo]\nbar

    \n", "example": 543, "start_line": 8163, "end_line": 8171, "section": "Links" }, { "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", "html": "

    bar

    \n", "example": 544, "start_line": 8204, "end_line": 8212, "section": "Links" }, { "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", "html": "

    [bar][foo!]

    \n", "example": 545, "start_line": 8219, "end_line": 8225, "section": "Links" }, { "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", "html": "

    [foo][ref[]

    \n

    [ref[]: /uri

    \n", "example": 546, "start_line": 8231, "end_line": 8238, "section": "Links" }, { "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n", "html": "

    [foo][ref[bar]]

    \n

    [ref[bar]]: /uri

    \n", "example": 547, "start_line": 8241, "end_line": 8248, "section": "Links" }, { "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", "html": "

    [[[foo]]]

    \n

    [[[foo]]]: /url

    \n", "example": 548, "start_line": 8251, "end_line": 8258, "section": "Links" }, { "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", "html": "

    foo

    \n", "example": 549, "start_line": 8261, "end_line": 8267, "section": "Links" }, { "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", "html": "

    bar\\

    \n", "example": 550, "start_line": 8272, "end_line": 8278, "section": "Links" }, { "markdown": "[]\n\n[]: /uri\n", "html": "

    []

    \n

    []: /uri

    \n", "example": 551, "start_line": 8284, "end_line": 8291, "section": "Links" }, { "markdown": "[\n ]\n\n[\n ]: /uri\n", "html": "

    [\n]

    \n

    [\n]: /uri

    \n", "example": 552, "start_line": 8294, "end_line": 8305, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", "html": "

    foo

    \n", "example": 553, "start_line": 8317, "end_line": 8323, "section": "Links" }, { "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

    foo bar

    \n", "example": 554, "start_line": 8326, "end_line": 8332, "section": "Links" }, { "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", "html": "

    Foo

    \n", "example": 555, "start_line": 8337, "end_line": 8343, "section": "Links" }, { "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

    foo\n[]

    \n", "example": 556, "start_line": 8350, "end_line": 8358, "section": "Links" }, { "markdown": "[foo]\n\n[foo]: /url \"title\"\n", "html": "

    foo

    \n", "example": 557, "start_line": 8370, "end_line": 8376, "section": "Links" }, { "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

    foo bar

    \n", "example": 558, "start_line": 8379, "end_line": 8385, "section": "Links" }, { "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n", "html": "

    [foo bar]

    \n", "example": 559, "start_line": 8388, "end_line": 8394, "section": "Links" }, { "markdown": "[[bar [foo]\n\n[foo]: /url\n", "html": "

    [[bar foo

    \n", "example": 560, "start_line": 8397, "end_line": 8403, "section": "Links" }, { "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", "html": "

    Foo

    \n", "example": 561, "start_line": 8408, "end_line": 8414, "section": "Links" }, { "markdown": "[foo] bar\n\n[foo]: /url\n", "html": "

    foo bar

    \n", "example": 562, "start_line": 8419, "end_line": 8425, "section": "Links" }, { "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

    [foo]

    \n", "example": 563, "start_line": 8431, "end_line": 8437, "section": "Links" }, { "markdown": "[foo*]: /url\n\n*[foo*]\n", "html": "

    *foo*

    \n", "example": 564, "start_line": 8443, "end_line": 8449, "section": "Links" }, { "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", "html": "

    foo

    \n", "example": 565, "start_line": 8455, "end_line": 8462, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url1\n", "html": "

    foo

    \n", "example": 566, "start_line": 8464, "end_line": 8470, "section": "Links" }, { "markdown": "[foo]()\n\n[foo]: /url1\n", "html": "

    foo

    \n", "example": 567, "start_line": 8474, "end_line": 8480, "section": "Links" }, { "markdown": "[foo](not a link)\n\n[foo]: /url1\n", "html": "

    foo(not a link)

    \n", "example": 568, "start_line": 8482, "end_line": 8488, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", "html": "

    [foo]bar

    \n", "example": 569, "start_line": 8493, "end_line": 8499, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n", "html": "

    foobaz

    \n", "example": 570, "start_line": 8505, "end_line": 8512, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n", "html": "

    [foo]bar

    \n", "example": 571, "start_line": 8518, "end_line": 8525, "section": "Links" }, { "markdown": "![foo](/url \"title\")\n", "html": "

    \"foo\"

    \n", "example": 572, "start_line": 8541, "end_line": 8545, "section": "Images" }, { "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

    \"foo

    \n", "example": 573, "start_line": 8548, "end_line": 8554, "section": "Images" }, { "markdown": "![foo ![bar](/url)](/url2)\n", "html": "

    \"foo

    \n", "example": 574, "start_line": 8557, "end_line": 8561, "section": "Images" }, { "markdown": "![foo [bar](/url)](/url2)\n", "html": "

    \"foo

    \n", "example": 575, "start_line": 8564, "end_line": 8568, "section": "Images" }, { "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

    \"foo

    \n", "example": 576, "start_line": 8578, "end_line": 8584, "section": "Images" }, { "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n", "html": "

    \"foo

    \n", "example": 577, "start_line": 8587, "end_line": 8593, "section": "Images" }, { "markdown": "![foo](train.jpg)\n", "html": "

    \"foo\"

    \n", "example": 578, "start_line": 8596, "end_line": 8600, "section": "Images" }, { "markdown": "My ![foo bar](/path/to/train.jpg \"title\" )\n", "html": "

    My \"foo

    \n", "example": 579, "start_line": 8603, "end_line": 8607, "section": "Images" }, { "markdown": "![foo]()\n", "html": "

    \"foo\"

    \n", "example": 580, "start_line": 8610, "end_line": 8614, "section": "Images" }, { "markdown": "![](/url)\n", "html": "

    \"\"

    \n", "example": 581, "start_line": 8617, "end_line": 8621, "section": "Images" }, { "markdown": "![foo][bar]\n\n[bar]: /url\n", "html": "

    \"foo\"

    \n", "example": 582, "start_line": 8626, "end_line": 8632, "section": "Images" }, { "markdown": "![foo][bar]\n\n[BAR]: /url\n", "html": "

    \"foo\"

    \n", "example": 583, "start_line": 8635, "end_line": 8641, "section": "Images" }, { "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", "html": "

    \"foo\"

    \n", "example": 584, "start_line": 8646, "end_line": 8652, "section": "Images" }, { "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

    \"foo

    \n", "example": 585, "start_line": 8655, "end_line": 8661, "section": "Images" }, { "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", "html": "

    \"Foo\"

    \n", "example": 586, "start_line": 8666, "end_line": 8672, "section": "Images" }, { "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

    \"foo\"\n[]

    \n", "example": 587, "start_line": 8678, "end_line": 8686, "section": "Images" }, { "markdown": "![foo]\n\n[foo]: /url \"title\"\n", "html": "

    \"foo\"

    \n", "example": 588, "start_line": 8691, "end_line": 8697, "section": "Images" }, { "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

    \"foo

    \n", "example": 589, "start_line": 8700, "end_line": 8706, "section": "Images" }, { "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", "html": "

    ![[foo]]

    \n

    [[foo]]: /url "title"

    \n", "example": 590, "start_line": 8711, "end_line": 8718, "section": "Images" }, { "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", "html": "

    \"Foo\"

    \n", "example": 591, "start_line": 8723, "end_line": 8729, "section": "Images" }, { "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

    ![foo]

    \n", "example": 592, "start_line": 8735, "end_line": 8741, "section": "Images" }, { "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", "html": "

    !foo

    \n", "example": 593, "start_line": 8747, "end_line": 8753, "section": "Images" }, { "markdown": "\n", "html": "

    http://foo.bar.baz

    \n", "example": 594, "start_line": 8780, "end_line": 8784, "section": "Autolinks" }, { "markdown": "\n", "html": "

    https://foo.bar.baz/test?q=hello&id=22&boolean

    \n", "example": 595, "start_line": 8787, "end_line": 8791, "section": "Autolinks" }, { "markdown": "\n", "html": "

    irc://foo.bar:2233/baz

    \n", "example": 596, "start_line": 8794, "end_line": 8798, "section": "Autolinks" }, { "markdown": "\n", "html": "

    MAILTO:FOO@BAR.BAZ

    \n", "example": 597, "start_line": 8803, "end_line": 8807, "section": "Autolinks" }, { "markdown": "\n", "html": "

    a+b+c:d

    \n", "example": 598, "start_line": 8815, "end_line": 8819, "section": "Autolinks" }, { "markdown": "\n", "html": "

    made-up-scheme://foo,bar

    \n", "example": 599, "start_line": 8822, "end_line": 8826, "section": "Autolinks" }, { "markdown": "\n", "html": "

    https://../

    \n", "example": 600, "start_line": 8829, "end_line": 8833, "section": "Autolinks" }, { "markdown": "\n", "html": "

    localhost:5001/foo

    \n", "example": 601, "start_line": 8836, "end_line": 8840, "section": "Autolinks" }, { "markdown": "\n", "html": "

    <https://foo.bar/baz bim>

    \n", "example": 602, "start_line": 8845, "end_line": 8849, "section": "Autolinks" }, { "markdown": "\n", "html": "

    https://example.com/\\[\\

    \n", "example": 603, "start_line": 8854, "end_line": 8858, "section": "Autolinks" }, { "markdown": "\n", "html": "

    foo@bar.example.com

    \n", "example": 604, "start_line": 8876, "end_line": 8880, "section": "Autolinks" }, { "markdown": "\n", "html": "

    foo+special@Bar.baz-bar0.com

    \n", "example": 605, "start_line": 8883, "end_line": 8887, "section": "Autolinks" }, { "markdown": "\n", "html": "

    <foo+@bar.example.com>

    \n", "example": 606, "start_line": 8892, "end_line": 8896, "section": "Autolinks" }, { "markdown": "<>\n", "html": "

    <>

    \n", "example": 607, "start_line": 8901, "end_line": 8905, "section": "Autolinks" }, { "markdown": "< https://foo.bar >\n", "html": "

    < https://foo.bar >

    \n", "example": 608, "start_line": 8908, "end_line": 8912, "section": "Autolinks" }, { "markdown": "\n", "html": "

    <m:abc>

    \n", "example": 609, "start_line": 8915, "end_line": 8919, "section": "Autolinks" }, { "markdown": "\n", "html": "

    <foo.bar.baz>

    \n", "example": 610, "start_line": 8922, "end_line": 8926, "section": "Autolinks" }, { "markdown": "https://example.com\n", "html": "

    https://example.com

    \n", "example": 611, "start_line": 8929, "end_line": 8933, "section": "Autolinks" }, { "markdown": "foo@bar.example.com\n", "html": "

    foo@bar.example.com

    \n", "example": 612, "start_line": 8936, "end_line": 8940, "section": "Autolinks" }, { "markdown": "\n", "html": "

    \n", "example": 613, "start_line": 9016, "end_line": 9020, "section": "Raw HTML" }, { "markdown": "\n", "html": "

    \n", "example": 614, "start_line": 9025, "end_line": 9029, "section": "Raw HTML" }, { "markdown": "\n", "html": "

    \n", "example": 615, "start_line": 9034, "end_line": 9040, "section": "Raw HTML" }, { "markdown": "\n", "html": "

    \n", "example": 616, "start_line": 9045, "end_line": 9051, "section": "Raw HTML" }, { "markdown": "Foo \n", "html": "

    Foo

    \n", "example": 617, "start_line": 9056, "end_line": 9060, "section": "Raw HTML" }, { "markdown": "<33> <__>\n", "html": "

    <33> <__>

    \n", "example": 618, "start_line": 9065, "end_line": 9069, "section": "Raw HTML" }, { "markdown": "
    \n", "html": "

    <a h*#ref="hi">

    \n", "example": 619, "start_line": 9074, "end_line": 9078, "section": "Raw HTML" }, { "markdown": "
    \n", "html": "

    <a href="hi'> <a href=hi'>

    \n", "example": 620, "start_line": 9083, "end_line": 9087, "section": "Raw HTML" }, { "markdown": "< a><\nfoo>\n\n", "html": "

    < a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />

    \n", "example": 621, "start_line": 9092, "end_line": 9102, "section": "Raw HTML" }, { "markdown": "
    \n", "html": "

    <a href='bar'title=title>

    \n", "example": 622, "start_line": 9107, "end_line": 9111, "section": "Raw HTML" }, { "markdown": "
    \n", "html": "

    \n", "example": 623, "start_line": 9116, "end_line": 9120, "section": "Raw HTML" }, { "markdown": "\n", "html": "

    </a href="foo">

    \n", "example": 624, "start_line": 9125, "end_line": 9129, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 625, "start_line": 9134, "end_line": 9140, "section": "Raw HTML" }, { "markdown": "foo foo -->\n\nfoo foo -->\n", "html": "

    foo foo -->

    \n

    foo foo -->

    \n", "example": 626, "start_line": 9142, "end_line": 9149, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 627, "start_line": 9154, "end_line": 9158, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 628, "start_line": 9163, "end_line": 9167, "section": "Raw HTML" }, { "markdown": "foo &<]]>\n", "html": "

    foo &<]]>

    \n", "example": 629, "start_line": 9172, "end_line": 9176, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 630, "start_line": 9182, "end_line": 9186, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 631, "start_line": 9191, "end_line": 9195, "section": "Raw HTML" }, { "markdown": "\n", "html": "

    <a href=""">

    \n", "example": 632, "start_line": 9198, "end_line": 9202, "section": "Raw HTML" }, { "markdown": "foo \nbaz\n", "html": "

    foo
    \nbaz

    \n", "example": 633, "start_line": 9212, "end_line": 9218, "section": "Hard line breaks" }, { "markdown": "foo\\\nbaz\n", "html": "

    foo
    \nbaz

    \n", "example": 634, "start_line": 9224, "end_line": 9230, "section": "Hard line breaks" }, { "markdown": "foo \nbaz\n", "html": "

    foo
    \nbaz

    \n", "example": 635, "start_line": 9235, "end_line": 9241, "section": "Hard line breaks" }, { "markdown": "foo \n bar\n", "html": "

    foo
    \nbar

    \n", "example": 636, "start_line": 9246, "end_line": 9252, "section": "Hard line breaks" }, { "markdown": "foo\\\n bar\n", "html": "

    foo
    \nbar

    \n", "example": 637, "start_line": 9255, "end_line": 9261, "section": "Hard line breaks" }, { "markdown": "*foo \nbar*\n", "html": "

    foo
    \nbar

    \n", "example": 638, "start_line": 9267, "end_line": 9273, "section": "Hard line breaks" }, { "markdown": "*foo\\\nbar*\n", "html": "

    foo
    \nbar

    \n", "example": 639, "start_line": 9276, "end_line": 9282, "section": "Hard line breaks" }, { "markdown": "`code \nspan`\n", "html": "

    code span

    \n", "example": 640, "start_line": 9287, "end_line": 9292, "section": "Hard line breaks" }, { "markdown": "`code\\\nspan`\n", "html": "

    code\\ span

    \n", "example": 641, "start_line": 9295, "end_line": 9300, "section": "Hard line breaks" }, { "markdown": "
    \n", "html": "

    \n", "example": 642, "start_line": 9305, "end_line": 9311, "section": "Hard line breaks" }, { "markdown": "\n", "html": "

    \n", "example": 643, "start_line": 9314, "end_line": 9320, "section": "Hard line breaks" }, { "markdown": "foo\\\n", "html": "

    foo\\

    \n", "example": 644, "start_line": 9327, "end_line": 9331, "section": "Hard line breaks" }, { "markdown": "foo \n", "html": "

    foo

    \n", "example": 645, "start_line": 9334, "end_line": 9338, "section": "Hard line breaks" }, { "markdown": "### foo\\\n", "html": "

    foo\\

    \n", "example": 646, "start_line": 9341, "end_line": 9345, "section": "Hard line breaks" }, { "markdown": "### foo \n", "html": "

    foo

    \n", "example": 647, "start_line": 9348, "end_line": 9352, "section": "Hard line breaks" }, { "markdown": "foo\nbaz\n", "html": "

    foo\nbaz

    \n", "example": 648, "start_line": 9363, "end_line": 9369, "section": "Soft line breaks" }, { "markdown": "foo \n baz\n", "html": "

    foo\nbaz

    \n", "example": 649, "start_line": 9375, "end_line": 9381, "section": "Soft line breaks" }, { "markdown": "hello $.;'there\n", "html": "

    hello $.;'there

    \n", "example": 650, "start_line": 9395, "end_line": 9399, "section": "Textual content" }, { "markdown": "Foo χρῆν\n", "html": "

    Foo χρῆν

    \n", "example": 651, "start_line": 9402, "end_line": 9406, "section": "Textual content" }, { "markdown": "Multiple spaces\n", "html": "

    Multiple spaces

    \n", "example": 652, "start_line": 9411, "end_line": 9415, "section": "Textual content" } ]elvish-0.21.0/pkg/md/stack.go000066400000000000000000000003421465720375400157150ustar00rootroot00000000000000package md type stack[T any] []T func (s *stack[T]) push(v T) { *s = append(*s, v) } func (s stack[T]) peek() T { return s[len(s)-1] } func (s *stack[T]) pop() T { last := s.peek() *s = (*s)[:len(*s)-1] return last } elvish-0.21.0/pkg/md/testdata/000077500000000000000000000000001465720375400160735ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/000077500000000000000000000000001465720375400170715ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRender/000077500000000000000000000000001465720375400242225ustar00rootroot0000000000000009165d96e6eede6b6057e300935fb1aa5c98243ed4f94b75a3ae31fb129e696d000066400000000000000000000000441465720375400347670ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[](<>(0))") 0d768707e28d09f6ef49b5e8c7e8f44fbea157f6296a05abb302ee5b77e3a168000066400000000000000000000000401465720375400350560ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("<&@0>") 17c530b620d9fbcf8870fe975ed11dd7062eb582d7ca3fc5ddbc293d5344e2f9000066400000000000000000000001151465720375400352010ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("999999990)\n0)\n0)\n0)\n0)\n0)\n0)\n0)\n0)\n0)\n0)") 2b69704609fc2e3745fba9a435b29b3858c597efe349cf8cc39c193e6f02173c000066400000000000000000000000411465720375400346420ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("\\ \n0") 2ced69587e727c3c37b87201e0e44e450090e6a195425a8fe0dc66bbc5163fd6000066400000000000000000000000421465720375400346120ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[]( <0)") 2ec9c489ff1c9ed8d8a24c2eb9e4033303149dcd9875ea82bf4764d0df144af5000066400000000000000000000000431465720375400351320ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("* ```\n0") 334ff8e8eaece3f84601f65db41d9cffa5634be1d23c4f97ea44edf4669f712f000066400000000000000000000000431465720375400353610ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string(" ***") 3463752fb4670a5a72d0033d9f2caee37023353a019825bb48223e7a01d9b760000066400000000000000000000000431465720375400342530ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("_0 _") 4310d034c2ad018a0649d15c7d6748bfad6779fb067b28b09debb146eb77bd31000066400000000000000000000000431465720375400347320ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*\n+ ***") 43d96cdf434f4f13cd32c391724d7c1cbc55d63d92195ea9da67c397abb722fa000066400000000000000000000000411465720375400351110ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string(">\\---") 45f9492476d79ae796da2ffeb3476370586553a5071b32bd1cc7a07fbd80356f000066400000000000000000000000411465720375400345510ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("* --") 46d002372d39c68359423d57dbe63c3c83003ecbd5e601286a5f80bc691624ef000066400000000000000000000000421465720375400344420ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("_00*0*_") 4951856280eb053ef3a943dcf6fb1e27cd595e4ab8aa00934517a702e71ca940000066400000000000000000000000421465720375400345760ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("- - - *") 49ef19e3514a8f95ff404e89d70d308b76d947b505a9593e2e72712048f2ae42000066400000000000000000000000441465720375400344250ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*\n
    ") 51e230c835df97b3aec428fe7a0be1704811421c5c28a78c9c3b74983157b1e4000066400000000000000000000000441465720375400345240ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("�*`0`*") 594ea6c06082c3fc565a1304ce5a809d2a37dac682163c23c73be7748bc5b464000066400000000000000000000000521465720375400345750ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*0\n ***\n0*") 5e0c9718d6e600b573a921052b15c44fb57a68a9d70af7a7c8899b33adefcc40000066400000000000000000000000521465720375400347520ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0\n ") 5fb0a2c38bf290e32115dd5c2de37cf22994dfaa3c2c74200060e8f6d0d0d2be000066400000000000000000000000701465720375400351240ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("_______0__!!!!!!!!!!!!!__!___") 6a6981e2afe37e15899b27af919cd50085520fdeab41051147b037418805bfd6000066400000000000000000000000421465720375400344500ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[](\\<)") 6ed9c798d44c6e3a7c68257ace3ece80a752882b1a06d06a101f375abc30fa32000066400000000000000000000000431465720375400350140ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*0!*�") 714ffe658ccfa9609ff79b93aa1462f9a1b4c1a4709c0256ae7bc4244578601c000066400000000000000000000000441465720375400346770ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*!00*\xb1") 722391ce85af71be128f8d7aa7b3620e9d4810458b8148c119256a93647fea3e000066400000000000000000000001051465720375400344620ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("00000000000000000000000000000000 _0000_*0*") 7425c53ab767b535434fe5e744491bc978146e002c5f6c701ea0ed3a7032bf89000066400000000000000000000000421465720375400344470ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("<0\\@0>") 763096c0f188a6f5a2cad16641570cfe484af56c6a406c3d89948406e4f11d62000066400000000000000000000000511465720375400344600ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[](0 (\n>))") 77f51992726987c5b66121aa85a2b57152ae80152ad2831a7b7d4c761a09ed2f000066400000000000000000000000401465720375400343640ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("\t~~~") 7ef0c0744eba12ae7f987c61107a4180b0772e25dd796045922da521e9a1b4cc000066400000000000000000000000421465720375400345660ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*![*]()") 7fd9444d106647162a0fbfff2f7c2f9da240565a759c0371cd5af930da19186c000066400000000000000000000000441465720375400346730ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("") 85278dc7594f426d6b7b4412586b262b25229d360f6c5fd07cc57839c048abee000066400000000000000000000000451465720375400344750ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0\n* 0) 00") 89d1293838e9db8b9ad5b72565dcf93f284492177b143bff251c8c5a35e96c48000066400000000000000000000000401465720375400345740ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("\t") 8becac1ca7e4e7c1456c91a25697a367a6b471b0c4a05e331456cd966003a108000066400000000000000000000000431465720375400345600ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("~~~ ") 8ec2e1d50f68c77c2a1e2707f9afa4794d33566315c26ae5c039390f55985a29000066400000000000000000000000421465720375400344710ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0000*`*") 95f31a02dbd47cd18ea3483ba9d91074fa80f371e144f9958c93a9f6e2a2e48a000066400000000000000000000000451465720375400347650ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("") 9e5f2825cc97261ce08d95ce1a30e32ea78945cd5946af20a92307506bf4fa53000066400000000000000000000000471465720375400346230ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("___ ___") a1b7773f70c10e0032514d1d0068a9b1d51b82e0a70adcd591858046509bb653000066400000000000000000000000451465720375400343260ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[](<\x00>)") aa47fa137928697726bba1b40b01f4e2a1c6980652433dd47b20d2517616c685000066400000000000000000000000441465720375400342720ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string(" 0") acbe5b22b3e9beb52a963220e2fe8e7e3f0f4bdcd7d0af4ab3815f69bbb6077b000066400000000000000000000000421465720375400355270ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("~~~ ") adb64ce0b0270d2fb724e07aa3dc7f0b7cb343708c008e021575b529d1e8395e000066400000000000000000000000441465720375400347140ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("# 0 0") c3eb680407bb9b0d9a2fc29b862115d798bccc3309fddb86395824cadba8f63d000066400000000000000000000000441465720375400351760ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string(">```\n\n>") c760e1f86fcf5e26acf8f17b1c9da06b8503d320f3642e82a4a8ea8a37a55910000066400000000000000000000000451465720375400350240ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("*[0***0]()") ca797b6dad7e8007aa973269e07ab8e08e078ef5c738b7bea8d136ca05cd7a4d000066400000000000000000000000451465720375400352650ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("![ \\\n]()") cd37db7c4899c0be1226816e12693c93970d1a5292f752293695b2490d98fea2000066400000000000000000000000501465720375400343360ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("") d66d86f848073d5c90685009906e1fb0840748fd8d3a452ee8cb3e6d3f1b29e9000066400000000000000000000000431465720375400345700ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("~~~\\\\!") d845768a00eefd7adde472aca51dbfb198601986e0f1c2ad6257b4afd4540176000066400000000000000000000000451465720375400351040ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("_0*0_ 0*00") d853024ac25e2be9f90784cfc58f28c496c9455b3c988b0964c02b45ac451b74000066400000000000000000000000451465720375400345540ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("___ 0") d8e4495c47b5e918fc92795426735bcc9ee5beaea5058a84bbccba4401c6386d000066400000000000000000000000451465720375400351340ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("* 0) * --") d9dbd4e786917123c3126420bb75315b0affdca4ef3fc5b521be051566b28479000066400000000000000000000000451465720375400346550ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0\n* * * +") de531027ab47da7f412a44d596530c35c32101ea60128c2328cdfd2a12cf20d3000066400000000000000000000000421465720375400345230ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("* *") e609714031cd68fb7e402746b4c842229d77388ac34e6fbcad3ce52431a6e2a6000066400000000000000000000000441465720375400346050ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("[]()") eba78016235f789ac52ed7a9b12064cb034f410c7ecd324dce7afba390b409a2000066400000000000000000000000471465720375400350610ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0\n ") f56cebbc6539e13372fc5723238d6ff42177d27e683121e0d287d85ecbbde400000066400000000000000000000000571465720375400347000ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("* * * 0\n *") f5a2058125b0d6bc9bd21817b8bedb3ed3fd6ee77b8dbcd12e3f6533e17aa335000066400000000000000000000000401465720375400353060ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("# \\#") fdb49d060e4e8905df6ecffcc3044a27cd7dc348ecc3c7e6049064e0e2b96ca1000066400000000000000000000000411465720375400353260ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzFmtPreservesHTMLRendergo test fuzz v1 string("0 ") elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespaces/000077500000000000000000000000001465720375400307475ustar00rootroot00000000000000179da62c2709fec804728d69facdd60c840f5cf2082ef74822ff6f7bc12a02e8000066400000000000000000000000511465720375400415670ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("--\n-") int(157) 467f234f387a81f6e97d86814ca835b444ea00a1a9470f100ce449d6b2beb3d8000066400000000000000000000000531465720375400413370ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("") int(103) 586bc16e05e19d40cc33415c9a08aef70f05c221fc3dbe850c1869c65d062b50000066400000000000000000000000521465720375400413700ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("*\\\n0*") int(75) 80832a884668db006bd77bf19e3758fe3413e528fd38966ed8086f956d5bc201000066400000000000000000000000471465720375400411650ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("\\\n0") int(4) ac5a3d974210f5d5bcbd47eb5a3ea1bb1e3c0091077981bedae9f9c7ab2449c1000066400000000000000000000000531465720375400420300ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("[](< >)") int(20) d0138fd295a0fa831075f449614b0a8b8cf421e8754463f1907d68ef05ef0475000066400000000000000000000000531465720375400411270ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("0

    \n0") int(80) e20e64463c3adce77239a55290f960552bc2bf611ef237800b77d0b25bc06cc6000066400000000000000000000000621465720375400413130ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string("000000000 \\\n0") int(20) e56bf66cc0560c5e7905445712d6fd4ef85fc0f0f55e304904fcba58645beacd000066400000000000000000000000501465720375400416420ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtPreservesHTMLRenderModuleWhitespacesgo test fuzz v1 string(" ") int(28) elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtResultFitsInWidth/000077500000000000000000000000001465720375400251715ustar00rootroot000000000000004addab28ca79b43124a432b0980b1504d78dc5ea7b8c722d4a3f960729240178000066400000000000000000000000541465720375400354570ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtResultFitsInWidthgo test fuzz v1 string("\x02 00") int(-118) c5523be82830755cd690a04810119e39290f0dc446c38e612dc0cc46ccc4cdaf000066400000000000000000000000511465720375400356130ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtResultFitsInWidthgo test fuzz v1 string("\\# 000") int(5) elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtResultIsUnchangedUnderFmt/000077500000000000000000000000001465720375400266325ustar00rootroot000000000000001f26a323e0bc46cde6ab0cdbf3d947e58eab9df8bbd189bda681902c307a0797000066400000000000000000000000531465720375400377330ustar00rootroot00000000000000elvish-0.21.0/pkg/md/testdata/fuzz/FuzzReflowFmtResultIsUnchangedUnderFmtgo test fuzz v1 string("\\\n ") int(17) elvish-0.21.0/pkg/md/testexport_test.go000066400000000000000000000000471465720375400200720ustar00rootroot00000000000000package md var EscapeURL = &escapeURL elvish-0.21.0/pkg/md/testutils_test.go000066400000000000000000000155661465720375400177250ustar00rootroot00000000000000package md_test import ( _ "embed" "encoding/json" "fmt" "regexp" "strings" "testing" "src.elv.sh/pkg/must" ) type testCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` Section string `json:"section"` Name string // supplemental cases only } func (tc *testCase) testName() string { if tc.Name != "" { return fmt.Sprintf("%s/%s", tc.Section, tc.Name) } return fmt.Sprintf("%s/Example %d", tc.Section, tc.Example) } func (tc *testCase) skipReason() string { switch tc.Section { case "Tabs", "Setext headings", "Link reference definitions": return "section not supported" } switch tc.Example { case 59, 115, 141, 300: return "setext heading not supported" case 23, 33, 317, // Link 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 549, 550, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 573, 576, 577, // Image 582, 583, 584, 585, 586, 587, 588, 589, 591, 592, 593: return "link reference definitions not supported" case 294, 296, 307, 318, 319, 320, 321, 323: return "tight list not supported" } return "" } func (tc *testCase) skipIfNotSupported(t *testing.T) { if reason := tc.skipReason(); reason != "" { t.Skip(reason) } } //go:embed spec/spec.json var specJSON []byte var specTestCases = readSpecTestCases(specJSON) var htmlTestCases = concat(specTestCases, supplementalHTMLTestCases) // When adding supplemental test cases, check a reference implementation to // determine the expected output. https://spec.commonmark.org/dingus (which uses // https://github.com/commonmark/commonmark.js) is the most convenient. var supplementalHTMLTestCases = []testCase{ { Section: "ATX headings", Name: "Attribute extension", Markdown: "# title {#id}", HTML: dedent(`

    title

    `), }, { Section: "Fenced code blocks", Name: "Empty line in list item", Markdown: "- ```\n a\n\n ```\n", HTML: dedent(`
    • a
      
      			
    `), }, { Section: "HTML blocks", Name: "Closed by lack of blockquote marker", Markdown: dedent(` >
    
    			a
    			`),
    		HTML: dedent(`
    			
    			

    a

    `), }, { Section: "HTML blocks", Name: "Closed by insufficient list item indentation", Markdown: dedent(` -
    			 a
    			`),
    		HTML: dedent(`
    			
    • 			

    a

    `), }, { Section: "Blockquotes", Name: "Increasing level", Markdown: dedent(` > a >> b `), HTML: dedent(`

    a

    b

    `), }, { Section: "Blockquotes", Name: "Reducing level", Markdown: dedent(` >> a > > b `), HTML: dedent(`

    a

    b

    `), }, { Section: "List items", Name: "Two leading empty lines with spaces", Markdown: dedent(` - a `), HTML: dedent(`

    a

    `), }, { Section: "List", Name: "Two-level bullet list with no content interrupting paragraph", Markdown: dedent(` a - - `), HTML: dedent(`

    a

    `), }, { Section: "List", Name: "Ordered list with non-1 start in bullet list interrupting paragraph", Markdown: dedent(` a - 2. `), HTML: dedent(`

    a

    `), }, { Section: "Emphasis and strong emphasis", Name: "Star after letter before punctuation does not start emphasis", Markdown: `a*$*`, HTML: `

    a*$*

    ` + "\n", }, { Section: "Links", Name: "Backslash and entity in destination", Markdown: `[a](\>)`, HTML: `

    a

    ` + "\n", }, { Section: "Links", Name: "Backslash and entity in title", Markdown: `[a](b (\>))`, HTML: `

    a

    ` + "\n", }, { Section: "Links", Name: "Unmatched ( in destination, with title", Markdown: `[a](http://( "b")`, HTML: "

    [a](http://( "b")

    \n", }, { Section: "Links", Name: "Unescaped ( in title started with (", Markdown: `[a](b (()))`, HTML: "

    [a](b (()))

    \n", }, { Section: "Links", Name: "Literal & in destination", Markdown: `[a](http://b?c&d)`, HTML: `

    a

    ` + "\n", }, { Section: "Image", Name: "Omit hard line break tag in alt", Markdown: dedent(` ![a\ b](c.png) `), HTML: dedent(`

    a
			b

    `), }, // This behavior is intentionally under-specified in the spec. The reference // implementations puts the raw HTML in the alt attribute, so we match their // behavior. // // CommonMark.js is inconsistent here and does not escape the < and > in the // alt attribute: https://github.com/commonmark/commonmark.js/issues/264 { Section: "Image", Name: "Keep raw HTML in alt", Markdown: "![a ](b.png)", HTML: `

    a <a></a>

    ` + "\n", }, // CommonMark.js has a bug and will not generate the expected output: // https://github.com/commonmark/commonmark.js/issues/263 { Section: "Autolinks", Name: "Entity", Markdown: ``, HTML: `

    http://>

    ` + "\n", }, { Section: "Raw HTML", Name: "unclosed <", Markdown: `a<`, HTML: "

    a<

    \n", }, { Section: "Raw HTML", Name: "unclosed bar `), ttyRender: ui.T(dedent(` foo bar `)), }, { name: "blockquote", markdown: dedent(` Quote: > foo >> lorem > > bar `), ttyRender: ui.T(dedent(` Quote: │ foo │ │ │ lorem │ │ bar `)), }, { name: "bullet list", markdown: dedent(` List: - one more - two more `), ttyRender: ui.T(dedent(` List: • one more • two more `)), }, { name: "ordered list", markdown: dedent(` List: 1. one more 1. two more `), ttyRender: ui.T(dedent(` List: 1. one more 2. two more `)), }, { name: "nested blocks", markdown: dedent(` > foo > - item > 1. one > 1. another > - another item `), ttyRender: ui.T(dedent(` │ foo │ │ • item │ │ 1. one │ │ 2. another │ │ • another item `)), }, // Highlight code block { name: "highlight", markdown: dedent(` Some code: ~~~foo bar code content ~~~ `), highlight: func(info, code string) ui.Text { return ui.T(fmt.Sprintf("(%s) %q\n", info, code)) }, ttyRender: ui.T(dedent(` Some code: (foo bar) "code content\n" `)), }, { name: "highlight missing trailing newline", markdown: dedent(` Some code: ~~~foo bar code content ~~~ `), highlight: func(info, code string) ui.Text { return ui.T(fmt.Sprintf("(%s) %q", info, code)) }, ttyRender: ui.T(dedent(` Some code: (foo bar) "code content\n" `)), }, // Inline { name: "text", markdown: "foo bar", ttyRender: ui.T("foo bar\n"), }, { name: "inline kbd tag", markdown: "Press Enter.", ttyRender: markLines( "Press Enter.", stylesheet, " ^^^^^ "), }, { name: "code span", markdown: "Use `put`.", ttyRender: markLines( "Use put.", stylesheet, " ___ "), }, { name: "emphasis", markdown: "Try *this*.", ttyRender: markLines( "Try this.", stylesheet, " //// "), }, { name: "strong emphasis", markdown: "Try **that**.", ttyRender: markLines( "Try that.", stylesheet, " #### "), }, { name: "link with absolute destination", markdown: "Visit [example](https://example.com).", ttyRender: markLines( "Visit example (https://example.com).", stylesheet, " _______ "), }, { name: "link with relative destination", markdown: "See [section X](#x) and [page Y](y.html).", ttyRender: markLines( "See section X and page Y.", stylesheet, " _________ ______ "), }, { name: "image", markdown: "![Example logo](https://example.com/logo.png)", ttyRender: ui.T("Image: Example logo (https://example.com/logo.png)\n"), }, { name: "autolink", markdown: "Visit .", ttyRender: ui.T("Visit https://example.com.\n"), }, { name: "hard line break", markdown: dedent(` foo\ bar `), ttyRender: ui.T("foo\nbar\n"), }, // ConvertRelativeLink { name: "rellink conversion", markdown: "See [a](a.html).", rellink: func(dest string) string { return "https://example.com/" + dest }, ttyRender: markLines( "See a (https://example.com/a.html).", stylesheet, " _ "), }, { name: "rellink conversion return empty string", markdown: "See [a](a.html).", rellink: func(dest string) string { return "" }, ttyRender: markLines( "See a.", stylesheet, " _ "), }, // Reflow { name: "reflow text", markdown: "foo bar lorem ipsum", width: 8, ttyRender: ui.T(dedent(` foo bar lorem ipsum `)), }, { name: "styled text on the same line when reflowing", markdown: "*foo bar* lorem ipsum", width: 8, ttyRender: markLines( "foo bar", stylesheet, "///////", "lorem", "ipsum"), }, { name: "styled text broken up when reflowing", markdown: "foo bar *lorem ipsum*", width: 8, ttyRender: markLines( "foo bar", "lorem", stylesheet, "/////", "ipsum", stylesheet, "/////"), }, { name: "multiple lines merged when reflowing", markdown: dedent(` foo bar `), width: 8, ttyRender: ui.T("foo bar\n"), }, { name: "hard line break when reflowing", markdown: dedent(` foo\ bar `), width: 8, ttyRender: ui.T(dedent(` foo bar `)), }, } func TestTTYCodec(t *testing.T) { for _, tc := range ttyTests { t.Run(tc.name, func(t *testing.T) { codec := TTYCodec{ Width: tc.width, HighlightCodeBlock: tc.highlight, ConvertRelativeLink: tc.rellink, } Render(tc.markdown, &codec) got := codec.Text() if !reflect.DeepEqual(got, tc.ttyRender) { t.Errorf("markdown: %s\ngot: %s\nwant:%s", hrFence(tc.markdown), hrFence(got.VTString()), hrFence(tc.ttyRender.VTString())) } }) } } func markLines(args ...any) ui.Text { // Add newlines to each line. // // TODO: Change ui.MarkLines to do this. for i := 0; i < len(args); i++ { switch arg := args[i].(type) { case string: args[i] = arg + "\n" case ui.RuneStylesheet: // Skip over the next argument i++ } } return ui.MarkLines(args...) } elvish-0.21.0/pkg/md/zstring.go000066400000000000000000000041251465720375400163130ustar00rootroot00000000000000// Code generated by "stringer -type=OpType,InlineOpType -output=zstring.go"; DO NOT EDIT. package md 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[OpThematicBreak-0] _ = x[OpHeading-1] _ = x[OpCodeBlock-2] _ = x[OpHTMLBlock-3] _ = x[OpParagraph-4] _ = x[OpBlockquoteStart-5] _ = x[OpBlockquoteEnd-6] _ = x[OpListItemStart-7] _ = x[OpListItemEnd-8] _ = x[OpBulletListStart-9] _ = x[OpBulletListEnd-10] _ = x[OpOrderedListStart-11] _ = x[OpOrderedListEnd-12] } const _OpType_name = "OpThematicBreakOpHeadingOpCodeBlockOpHTMLBlockOpParagraphOpBlockquoteStartOpBlockquoteEndOpListItemStartOpListItemEndOpBulletListStartOpBulletListEndOpOrderedListStartOpOrderedListEnd" var _OpType_index = [...]uint8{0, 15, 24, 35, 46, 57, 74, 89, 104, 117, 134, 149, 167, 183} func (i OpType) String() string { if i >= OpType(len(_OpType_index)-1) { return "OpType(" + strconv.FormatInt(int64(i), 10) + ")" } return _OpType_name[_OpType_index[i]:_OpType_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[OpText-0] _ = x[OpCodeSpan-1] _ = x[OpRawHTML-2] _ = x[OpNewLine-3] _ = x[OpEmphasisStart-4] _ = x[OpEmphasisEnd-5] _ = x[OpStrongEmphasisStart-6] _ = x[OpStrongEmphasisEnd-7] _ = x[OpLinkStart-8] _ = x[OpLinkEnd-9] _ = x[OpImage-10] _ = x[OpAutolink-11] _ = x[OpHardLineBreak-12] } const _InlineOpType_name = "OpTextOpCodeSpanOpRawHTMLOpNewLineOpEmphasisStartOpEmphasisEndOpStrongEmphasisStartOpStrongEmphasisEndOpLinkStartOpLinkEndOpImageOpAutolinkOpHardLineBreak" var _InlineOpType_index = [...]uint8{0, 6, 16, 25, 34, 49, 62, 83, 102, 113, 122, 129, 139, 154} func (i InlineOpType) String() string { if i >= InlineOpType(len(_InlineOpType_index)-1) { return "InlineOpType(" + strconv.FormatInt(int64(i), 10) + ")" } return _InlineOpType_name[_InlineOpType_index[i]:_InlineOpType_index[i+1]] } elvish-0.21.0/pkg/mods/000077500000000000000000000000001465720375400146245ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/daemon/000077500000000000000000000000001465720375400160675ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/daemon/daemon.go000066400000000000000000000013461465720375400176650ustar00rootroot00000000000000// 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() any { 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]any{ "pid": getPid, }).Ns() } elvish-0.21.0/pkg/mods/daemon/daemon_test.go000066400000000000000000000001151465720375400207150ustar00rootroot00000000000000package daemon import "testing" func TestDaemon(t *testing.T) { // TODO } elvish-0.21.0/pkg/mods/doc/000077500000000000000000000000001465720375400153715ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/doc/doc.d.elv000066400000000000000000000050761465720375400171000ustar00rootroot00000000000000# Shows documentation for `$symbol` in the terminal. # # If `$symbol` starts with `$`, it is treated as a variable. Otherwise it is # treated as a function. # # Symbols in a module should be specified using a qualified name as if the # module is imported without renaming, like `doc:source`. Symbols in the builtin # module can be specified either in the unqualified form (like `put`) or with # the explicit `builtin:` namespace (like `builtin:put`). # # The `&width` option specifies the width to wrap the output to. If it is 0 (the # default) or negative, `show` queries the terminal width of the standard output # and use it as the width, falling back to 80 if the query fails (for example # when the standard output is not a terminal). # # This command is roughly equivalent to `md:show &width=$width (doc:show # $symbol)`, but has some extra processing of relative links to point them to # the Elvish website. # # Examples: # # ```elvish-transcript # ~> doc:show put # [ omitted ] # ~> doc:show builtin:put # [ omitted ] # ~> doc:show '$paths' # [ omitted ] # ~> doc:show doc:show # [ omitted ] # ``` # # See also [`md:show`](). fn show {|symbol &width=0| } # Finds symbols whose documentation contains all strings in `$queries`. # # The search is done on a version of the documentation with no markup and soft # line breaks converted to spaces. For example, if the Markdown source of a # symbol contains `foo *bar* [link](dest.html)`, with possible soft line breaks # in between, it will be matched by a query of `foo bar link`. # # The output shows each symbol that matches, followed by an excerpt of their # documentation with the matched queries highlighted. # # Examples: # # ```elvish-transcript # ~> doc:find namespace # ns: # Constructs a namespace from $map, using the keys as variable names and the values as their values. … # has-env: # … This command has no equivalent operation using the E: namespace (but see https://b.elv.sh/1026). # eval: # … The evaluation happens in a new, restricted namespace, whose initial set of variables can be specified by the &ns option. … # [ … more output omitted … ] # ~> doc:find namespace REPL # edit:add-var: # Defines a new variable in the interactive REPL with an initial value. … # This is most useful for modules to modify the REPL namespace. … # ``` fn find {|@queries| } # Outputs the Markdown source of the documentation for `$symbol` as a string # value. The `$symbol` arguments follows the same format as # [`doc:show`](). # # Examples: # # ```elvish-transcript # ~> doc:source put # ▶ "... omitted " # ``` fn source {|symbol| } elvish-0.21.0/pkg/mods/doc/doc.go000066400000000000000000000074551465720375400165000ustar00rootroot00000000000000// Package doc implements the doc: module. package doc import ( "fmt" "sort" "strings" "sync" "src.elv.sh/pkg" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/md" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/sys" ) var Ns = eval.BuildNsNamed("doc"). AddGoFns(map[string]any{ "show": show, "find": find, "source": Source, "-symbols": symbols, }). Ns() type showOptions struct{ Width int } func (opts *showOptions) SetDefaultOptions() {} func show(fm *eval.Frame, opts showOptions, fqname string) error { doc, err := Source(fqname) if err != nil { return err } width := opts.Width if width <= 0 { _, width = sys.WinSize(fm.Port(1).File) if width <= 0 { width = 80 } } codec := &md.TTYCodec{ Width: width, HighlightCodeBlock: elvdoc.HighlightCodeBlock, ConvertRelativeLink: func(dest string) string { // TTYCodec does not show destinations of relative links by default. // Special-case links to language.html as they are quite common in // elvdocs. if strings.HasPrefix(dest, "language.html") { return "https://elv.sh/ref/" + dest } return "" }, } if !strings.HasPrefix(fqname, "$") { // Function docs start with a code block that shows how to use the // function. Since the code block is indented 2 spaces by TTYCodec, it // looks a little bit weird as the first line of the output. Make the // output look slightly nicer by prepending a line. doc = "Usage:\n\n" + doc } _, err = fm.ByteOutput().WriteString(md.RenderString(doc, codec)) return err } func find(fm *eval.Frame, qs ...string) { for _, docs := range docsMap() { findIn := func(name, markdown string) { if bs, ok := match(markdown, qs); ok { out := fm.ByteOutput() fmt.Fprintf(out, "%s:\n", name) for _, b := range bs { fmt.Fprintf(out, " %s\n", b.Show()) } } } for _, entry := range docs.Fns { findIn(entry.Name, entry.FullContent()) } for _, entry := range docs.Vars { findIn(entry.Name, entry.FullContent()) } } } // Source returns the doc source for a symbol. func Source(qname string) (string, error) { isVar := strings.HasPrefix(qname, "$") var ns string if strings.ContainsRune(qname, ':') { if isVar { first, rest := eval.SplitQName(qname[1:]) if first == "builtin:" { // Normalize $builtin:foo -> $foo, and leave ns as "" qname = "$" + rest } else { ns = first } } else { first, rest := eval.SplitQName(qname) if first == "builtin:" { // Normalize builtin:foo -> foo, and leave ns as "" qname = rest } else { ns = first } } } docs, ok := docsMap()[ns] if !ok { return "", fmt.Errorf("no doc for %s", parse.Quote(qname)) } var entries []elvdoc.Entry if isVar { entries = docs.Vars } else { entries = docs.Fns } for _, entry := range entries { if entry.Name == qname { return entry.FullContent(), nil } } return "", fmt.Errorf("no doc for %s", parse.Quote(qname)) } func symbols(fm *eval.Frame) error { var names []string for _, docs := range docsMap() { for _, fn := range docs.Fns { names = append(names, fn.Name) } for _, v := range docs.Vars { names = append(names, v.Name) } } sort.Strings(names) for _, name := range names { err := fm.ValueOutput().Put(name) if err != nil { return err } } return nil } // Can be overridden in tests. var docsMapWithError = sync.OnceValues(func() (map[string]elvdoc.Docs, error) { return elvdoc.ExtractAllFromFS(pkg.ElvFiles) }) // Returns a map from namespace prefixes (like "doc:", or "" for the builtin // module) to extracted elvdocs. func docsMap() map[string]elvdoc.Docs { // docsMapWithError depends on an embedded FS and returns the same values // every time. The error is checked in a test, so we don't have to check it // during runtime. m, _ := docsMapWithError() return m } elvish-0.21.0/pkg/mods/doc/doc_test.elvts000066400000000000000000000036161465720375400202620ustar00rootroot00000000000000//each:eval use doc //////////// # doc:show # //////////// ## function ## ~> doc:show foo:function Usage: foo:function $x A function with long documentation. Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ## variable ## ~> doc:show '$foo:variable' A variable. Lorem ipsum. ## &width ## ~> doc:show &width=30 foo:function Usage: foo:function $x A function with long documentation. Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ## implicit builtin ## ~> doc:show break Usage: break Terminates a loop. ## explicit builtin ## ~> doc:show builtin:break Usage: break Terminates a loop. ## relative links ## // Relative links to language.html are converted to absolute links to // https://elv.sh/ref/language.html, but other relative links are not. ~> doc:show num Usage: num $x Constructs a typed number (https://elv.sh/ref/language.html#number). Another link. ## existing module, non-existing symbol ## ~> doc:show foo:bad Exception: no doc for foo:bad [tty]:1:1-16: doc:show foo:bad ## non-existing module ## ~> doc:show bad:foo Exception: no doc for bad:foo [tty]:1:1-16: doc:show bad:foo //////////// # doc:find # //////////// ~> doc:find ipsum foo:function: … Lorem ipsum dolor sit amet. … $foo:variable: … Lorem ipsum. ////////////// # doc:source # ////////////// // The implementation of doc:source is used by doc:show internally and exercised // in its tests, so we only test a simple case here. ~> doc:source '$foo:variable' ▶ "A variable. Lorem ipsum.\n" //////////////// # doc:-symbols # //////////////// ~> doc:-symbols // Note: symbols are sorted ▶ '$foo:variable' ▶ break ▶ foo:function ▶ num ## output error ## ~> doc:-symbols >&- Exception: port does not support value output [tty]:1:1-16: doc:-symbols >&- elvish-0.21.0/pkg/mods/doc/doc_test.go000066400000000000000000000015241465720375400175260ustar00rootroot00000000000000package doc_test import ( "embed" "io/fs" "testing" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) //go:embed fakepkg var fakepkg embed.FS //go:embed *.elvts var transcripts embed.FS func TestDocExtractionError(t *testing.T) { _, err := (*doc.DocsMapWithError)() if err != nil { t.Errorf("doc extraction has error: %v", err) } } func TestTranscripts(t *testing.T) { testutil.Set(t, doc.DocsMapWithError, func() (map[string]elvdoc.Docs, error) { return must.OK1(elvdoc.ExtractAllFromFS(must.OK1(fs.Sub(fakepkg, "fakepkg")))), nil }) // The result of reading the FS is cached. As a result, this override can't // be reverted, so we just do it here instead of properly inside a setup // function. evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/doc/fakepkg/000077500000000000000000000000001465720375400170015ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/doc/fakepkg/eval/000077500000000000000000000000001465720375400177305ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/doc/fakepkg/eval/break.md000066400000000000000000000000601465720375400213320ustar00rootroot00000000000000Usage: ```elvish break ``` Terminates a loop. elvish-0.21.0/pkg/mods/doc/fakepkg/eval/builtin.d.elv000066400000000000000000000001761465720375400223340ustar00rootroot00000000000000# Terminates a loop. fn break { } # Constructs a [typed number](language.html#number). Another # [link](#foo). fn num {|x| } elvish-0.21.0/pkg/mods/doc/fakepkg/eval/num.md000066400000000000000000000003341465720375400210510ustar00rootroot00000000000000Usage: ```elvish num $x ``` Constructs a [typed number](https://elv.sh/ref/language.html#number). Another [link](#foo). elvish-0.21.0/pkg/mods/doc/fakepkg/mods/000077500000000000000000000000001465720375400177435ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/doc/fakepkg/mods/foo/000077500000000000000000000000001465720375400205265ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/doc/fakepkg/mods/foo/foo.d.elv000066400000000000000000000003421465720375400222420ustar00rootroot00000000000000# A variable. Lorem ipsum. var variable # A function with long documentation. Lorem ipsum dolor sit amet. # Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut # labore et dolore magna aliqua. fn function {|x| } elvish-0.21.0/pkg/mods/doc/fakepkg/mods/foo/function.md000066400000000000000000000003071465720375400226750ustar00rootroot00000000000000Usage: ```elvish foo:function $x ``` A function with long documentation. Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. elvish-0.21.0/pkg/mods/doc/fakepkg/mods/foo/variable.md000066400000000000000000000000311465720375400226270ustar00rootroot00000000000000A variable. Lorem ipsum. elvish-0.21.0/pkg/mods/doc/match.go000066400000000000000000000067751465720375400170330ustar00rootroot00000000000000package doc import ( "sort" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/md" "src.elv.sh/pkg/ui" ) func match(markdown string, qs []string) ([]matchedBlock, bool) { var codec md.TextCodec md.Render(markdown, &codec) bs := codec.Blocks() bMatches := make([][]diag.Ranging, len(bs)) for _, q := range qs { qMatchesAny := false for i, b := range bs { if from := strings.Index(b.Text, q); from != -1 { bMatches[i] = append(bMatches[i], diag.Ranging{From: from, To: from + len(q)}) qMatchesAny = true break } } if !qMatchesAny { return nil, false } } var matched []matchedBlock for i, b := range bs { if len(bMatches[i]) > 0 { matched = append(matched, matchedBlock{b, sortAndMergeMatches(bMatches[i])}) } } return matched, true } func sortAndMergeMatches(rs []diag.Ranging) []diag.Ranging { sort.Slice(rs, func(i, j int) bool { return rs[i].From < rs[j].From }) i := 0 for j := 1; j < len(rs); j++ { if rs[j].From > rs[j-1].To { i++ rs[i] = rs[j] } else { rs[i].To = rs[j].To } } return rs[:i+1] } type matchedBlock struct { block md.TextBlock matches []diag.Ranging } var queryStyling = ui.Stylings(ui.Bold, ui.FgRed) func (b matchedBlock) Show() string { var sb strings.Builder // The algorithms to highlight queries in code blocks and queries in normal // text are quite similar, with one subtle difference: code blocks can // contain newlines, but we avoid writing them to keep the output tidy. // Newlines can arise when two adjacent lines are shown, or when a query // spans multiple lines. Both get replaced with " … ". if b.block.Code { lastTo := 0 lastLineTo := 0 for _, m := range b.matches { lineFrom := lastLineStart(b.block.Text, m.From) if lastLineTo < lineFrom { sb.WriteString(b.block.Text[lastTo:lastLineTo]) if sb.Len() > 0 { sb.WriteByte(' ') } sb.WriteString("… ") sb.WriteString(b.block.Text[lineFrom:m.From]) } else { sb.WriteString(b.block.Text[lastTo:m.From]) } queryText := strings.ReplaceAll(b.block.Text[m.From:m.To], "\n", " … ") sb.WriteString(ui.T(queryText, queryStyling).String()) lastTo = m.To lastLineTo = firstLineEnd(b.block.Text, m.To) } sb.WriteString(b.block.Text[lastTo:lastLineTo]) if lastLineTo < len(b.block.Text) { sb.WriteString(" …") } } else { lastTo := 0 lastSentenceTo := 0 for _, m := range b.matches { sentenceFrom := lastSentenceStart(b.block.Text, m.From) if lastSentenceTo < sentenceFrom { sb.WriteString(b.block.Text[lastTo:lastSentenceTo]) sb.WriteString("… ") sb.WriteString(b.block.Text[sentenceFrom:m.From]) } else { sb.WriteString(b.block.Text[lastTo:m.From]) } sb.WriteString(ui.T(b.block.Text[m.From:m.To], queryStyling).String()) lastTo = m.To lastSentenceTo = firstSentenceStart(b.block.Text, m.To) } sb.WriteString(b.block.Text[lastTo:lastSentenceTo]) if lastSentenceTo < len(b.block.Text) { sb.WriteString("…") } } return sb.String() } func firstSentenceStart(s string, from int) int { if i := strings.Index(s[from:], ". "); i != -1 { return from + i + 2 } return len(s) } func lastSentenceStart(s string, upto int) int { if i := strings.LastIndex(s[:upto], ". "); i != -1 { return i + 2 } return 0 } func firstLineEnd(s string, from int) int { if i := strings.Index(s[from:], "\n"); i != -1 { return from + i } return len(s) } func lastLineStart(s string, upto int) int { if i := strings.LastIndex(s[:upto], "\n"); i != -1 { return i + 1 } return 0 } elvish-0.21.0/pkg/mods/doc/match_test.go000066400000000000000000000130101465720375400200460ustar00rootroot00000000000000package doc_test import ( "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/testutil" "src.elv.sh/pkg/ui" ) var Dedent = testutil.Dedent var matchAndShowTests = []struct { name string markdown string qs []string want []string }{ { name: "no match", markdown: "Here is some doc", qs: []string{"foo"}, want: nil, }, { name: "not all queries match", markdown: "Here is some doc", qs: []string{"some", "foo"}, want: nil, }, { name: "one query matches one block", markdown: "Here is some doc", qs: []string{"some"}, want: []string{"Here is {some} doc"}, }, { name: "multiple queries match one block", markdown: "Here is some doc", qs: []string{"some", "doc"}, want: []string{"Here is {some} {doc}"}, }, { name: "multiple queries match one block, reverse order", markdown: "Here is some doc", qs: []string{"doc", "some"}, want: []string{"Here is {some} {doc}"}, }, { name: "one query matches multiple blocks, only first matched block shown", markdown: Dedent(` Here is some doc Here is some more doc `), qs: []string{"some"}, want: []string{"Here is {some} doc"}, }, { name: "multiple queries match multiple blocks respectively", markdown: Dedent(` Here is some doc Here is some more doc `), qs: []string{"some", "more"}, want: []string{ "Here is {some} doc", "Here is some {more} doc", }, }, { name: "overlapping matches", markdown: "Here is some doc", qs: []string{"is some", "some doc"}, want: []string{"Here {is some doc}"}, }, { name: "match in first sentence", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"some"}, want: []string{"Here is {some} doc. …"}, }, { name: "match in last sentence", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"even"}, want: []string{"… Here is {even} more."}, }, { name: "match in middle sentence", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"is more"}, want: []string{"… Here {is more}. …"}, }, { name: "matches in adjacent sentences", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"some", "more"}, want: []string{"Here is {some} doc. Here is {more}. …"}, }, { name: "matches in non-adjacent sentences", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"some", "even"}, want: []string{"Here is {some} doc. … Here is {even} more."}, }, { name: "multiple matches in one sentence", markdown: "Here is some doc. Here is more.", qs: []string{"some", "doc"}, want: []string{"Here is {some} {doc}. …"}, }, { name: "one match spanning multiple sentences", markdown: "Here is some doc. Here is more. Here is even more.", qs: []string{"some doc. Here", "even"}, want: []string{"Here is {some doc. Here} is more. Here is {even} more."}, }, { name: "match in first line of code block", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"foo"}, want: []string{"echo {foo} …"}, }, { name: "match in last line of code block", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"lorem"}, want: []string{"… echo {lorem}"}, }, { name: "match in middle line of code block", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"bar"}, want: []string{"… echo {bar} …"}, }, { name: "matches in adjacent lines", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"foo", "bar"}, want: []string{"echo {foo} … echo {bar} …"}, }, { name: "matches in non-adjacent lines", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"foo", "lorem"}, want: []string{"echo {foo} … echo {lorem}"}, }, { name: "multiple matches in one line", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"ch", "fo"}, want: []string{"e{ch}o {fo}o …"}, }, { name: "one match spanning multiple lines", markdown: codeBlockWithLines("echo foo", "echo bar", "echo lorem"), qs: []string{"foo\necho"}, want: []string{"echo {foo … echo} bar …"}, }, } func codeBlockWithLines(lines ...string) string { return "```\n" + strings.Join(lines, "\n") + "\n```" } // Test match and matchedBlock.Show together. They are used together in actual // production code and easier to test together. func TestMatchAndShow(t *testing.T) { for _, tc := range matchAndShowTests { t.Run(tc.name, func(t *testing.T) { want := tc.want for i := range want { want[i] = highlightBraced(want[i]) } got := matchAndShow(tc.markdown, tc.qs) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("unexpected result (-want +got):\n%s", diff) } }) } } func matchAndShow(markdown string, qs []string) []string { bs, ok := doc.Match(markdown, qs) if !ok { return nil } shown := make([]string, len(bs)) for i, b := range bs { shown[i] = b.Show() } return shown } var braced = regexp.MustCompile(`\{.*?\}`) func highlightBraced(s string) string { return braced.ReplaceAllStringFunc(s, func(p string) string { return ui.T(p[1:len(p)-1], ui.Bold, ui.FgRed).VTString() }) } elvish-0.21.0/pkg/mods/doc/testexport_test.go000066400000000000000000000001251465720375400211760ustar00rootroot00000000000000package doc var ( DocsMapWithError = &docsMapWithError Match = match ) elvish-0.21.0/pkg/mods/epm/000077500000000000000000000000001465720375400154055ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/epm/epm.elv000066400000000000000000000251431465720375400167030ustar00rootroot00000000000000use os use path use re use str use platform pragma unknown-command = disallow var dirname~ = $e:dirname~ var mkdir~ = $e:mkdir~ var git~ = $e:git~ var rsync~ = $e:rsync~ # 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 ] ] # 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 } # Returns a boolean value indicating whether the given package is installed. fn is-installed {|pkg| path:is-dir (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 } # 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 } catch _ { -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 (path:is-regular $cfgfile) { # If the config file exists, read it... set cfg = (from-json < $cfgfile) -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 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 os:remove-all $dest } ###################################################################### # Main user-facing functions # 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. 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) (path:is-regular $file)) { set res = (-merge (from-json < $file) $res) } put $res } # Pretty print the available metadata of the given package. fn query {|pkg| var data = (metadata $pkg) 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] var special-keys = [name method installed src dst] keys $data | each {|key| if (has-value $special-keys $key) { continue } var val = $data[$key] if (eq (kind-of $val) list) { set val = (str:join ", " $val) } echo (styled (str:title $key)":" blue) $val } } # Return an array with all installed packages. `epm:list` can be used as an alias # for `epm:installed`. 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 } # 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. fn install {|&silent-if-installed=$false @pkgs| # Install and upgrade are method-specific, so we call the # corresponding functions using -package-op 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 } catch e { -error "Dependency installation failed. Uninstalling "$pkg", please check the errors above and try again." -uninstall-package $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 } } } # Uninstall named packages. fn uninstall {|@pkgs| # Uninstall is the same for everyone, just remove the directory if (eq $pkgs []) { -error 'You must specify at least one package.' return } for pkg $pkgs { -uninstall-package $pkg } } elvish-0.21.0/pkg/mods/epm/epm.go000066400000000000000000000001711465720375400165140ustar00rootroot00000000000000package epm import _ "embed" // Code contains the source code of the epm module. // //go:embed epm.elv var Code string elvish-0.21.0/pkg/mods/epm/epm_test.elvts000066400000000000000000000001301465720375400202760ustar00rootroot00000000000000//prepare-deps // A smoke test to ensure that the epm module has no errors. ~> use epm elvish-0.21.0/pkg/mods/epm/epm_test.go000066400000000000000000000004101465720375400175470ustar00rootroot00000000000000package epm_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "prepare-deps", mods.AddTo) } elvish-0.21.0/pkg/mods/file/000077500000000000000000000000001465720375400155435ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/file/file.d.elv000066400000000000000000000105531465720375400174200ustar00rootroot00000000000000# Outputs whether `$file` is a terminal device. # # The `$file` can be a file object or a number. If it's a number, it's # interpreted as the number of an [IO port](language.html#io-ports). # # ```elvish-transcript # ~> var f = (file:open /dev/tty) # ~> file:is-tty $f # ▶ $true # ~> file:close $f # ~> var f = (file:open /dev/null) # ~> file:is-tty $f # ▶ $false # ~> file:close $f # ~> var p = (file:pipe) # ~> file:is-tty $p[r] # ▶ $false # ~> file:is-tty $p[w] # ▶ $false # ~> file:close $p[r] # ~> file:close $p[w] # ~> file:is-tty 0 # ▶ $true # ~> file:is-tty 1 # ▶ $true # ~> file:is-tty 2 # ▶ $true # ~> file:is-tty 0 < /dev/null # ▶ $false # ~> file:is-tty 0 < /dev/tty # ▶ $true # ``` fn is-tty {|file| } # Opens a file for input. The file must be closed with [`file:close`]() when no # longer needed. # # Example: # # ```elvish-transcript # ~> cat a.txt # This is # a file. # ~> use file # ~> var f = (file:open a.txt) # ~> cat < $f # This is # a file. # ~> file:close $f # ``` # # See also [`file:open-output`]() and [`file:close`](). fn open {|filename| } # Opens a file for output. The file must be closed with [`file:close`]() when no # longer needed. # # If `&also-input` is true, the file may also be used for input. # # The `&if-not-exists` option can be either `create` or `error`. # # The `&if-exists` option can be either `truncate` (removing all data), `append` # (appending to the end), `update` (updating in place) or `error`. The `error` # value may only be used with `&if-not-exists=create`. # # The `&create-perm` option specifies what permission to create the file with if # the file doesn't exist and `&if-not-exists=create`. It must be an integer # within [0, 0o777]. On Unix, the actual file permission is subject to filtering # by [`$unix:umask`](). # # Example: # # ```elvish-transcript # ~> use file # ~> var f = (file:open-output new) # ~> echo content > $f # ~> file:close $f # ~> cat new # content # ``` # # See also [`file:open`]() and [`file:close`](). fn open-output {|filename &also-input=$false &if-not-exists=create &if-exists=truncate &create-perm=(num 0o644)| } # Closes a file opened with `open`. # # See also [`file:open`](). fn close {|file| } # Creates a new pipe that can be used in redirections. Outputs a map with two # fields: `r` contains the read-end of the pipe, and `w` contains the write-end. # Both are [file object](language.html#file)) # # 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. Both ends of the 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 # ~> var 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 # ``` # # See also [`file:close`](). fn pipe { } # Sets the offset for the next read or write operation on `$file`. # # The `&whence` option specifies what the offset is relative to, and can be # `start`, `current` or `end`. # # The behavior of `seek` on a file opened using [`file:open-output`]() with # `&if-exists=append` is unspecified. # # Example: # # ```elvish-transcript # ~> print 0123456789 > file # ~> var f = (file:open file) # ~> read-bytes 3 < $f # ▶ 012 # ~> file:seek $f 0 # ~> read-bytes 3 < $f # ▶ 012 # ~> file:seek $f 2 &whence=current # ~> read-bytes 3 < $f # ▶ 567 # ~> file:seek $f -3 &whence=end # ~> read-bytes 3 < $f # ▶ 789 # ~> file:close $f # ``` fn seek {|file offset &whence=start| } # Outputs the current offset within `$file`, relative to the start of the file. # # Example: # # ```elvish-transcript # ~> print 0123456789 > file # ~> var f = (file:open file) # ~> read-bytes 3 < $f # ▶ 012 # ~> file:tell $f # ▶ (num 3) # ~> file:close $f # ``` fn tell {|file| } # 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. fn truncate {|filename size| } elvish-0.21.0/pkg/mods/file/file.go000066400000000000000000000103321465720375400170100ustar00rootroot00000000000000package file import ( "errors" "fmt" "io" "io/fs" "math" "math/big" "os" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" "src.elv.sh/pkg/sys" ) var Ns = eval.BuildNsNamed("file"). AddGoFns(map[string]any{ "close": close, "is-tty": isTTY, "open": open, "open-output": openOutput, "pipe": pipe, "seek": seek, "tell": tell, "truncate": truncate, }).Ns() func isTTY(fm *eval.Frame, file any) (bool, error) { switch file := file.(type) { case *os.File: return sys.IsATTY(file.Fd()), nil case int: return isTTYPort(fm, file), nil case string: var fd int if err := vals.ScanToGo(file, &fd); err != nil { return false, errs.BadValue{What: "argument to file:is-tty", Valid: "file value or numerical FD", Actual: parse.Quote(file)} } return isTTYPort(fm, fd), nil default: return false, errs.BadValue{What: "argument to file:is-tty", Valid: "file value or numerical FD", Actual: vals.ToString(file)} } } func isTTYPort(fm *eval.Frame, portNum int) bool { p := fm.Port(portNum) return p != nil && sys.IsATTY(p.File.Fd()) } func open(name string) (vals.File, error) { return os.Open(name) } type openOutputOpts struct { AlsoInput bool IfNotExists string IfExists string CreatePerm int } func (opts *openOutputOpts) SetDefaultOptions() { opts.IfNotExists = "create" opts.IfExists = "truncate" opts.CreatePerm = 0o644 } var errIfNotExistsAndIfExistsBothError = errors.New("both &if-not-exists and &if-exists are error") func openOutput(opts openOutputOpts, name string) (vals.File, error) { perm := opts.CreatePerm if perm < 0 || perm > 0o777 { return nil, errs.OutOfRange{What: "create-perm option", ValidLow: "0", ValidHigh: "0o777", Actual: fmt.Sprintf("%O", perm)} } mode := os.O_WRONLY if opts.AlsoInput { mode = os.O_RDWR } switch opts.IfNotExists { case "create": mode |= os.O_CREATE case "error": // Do nothing: not creating is the default. default: return nil, errs.BadValue{What: "if-not-exists option", Valid: "create or error", Actual: parse.Quote(opts.IfNotExists)} } switch opts.IfExists { case "truncate": mode |= os.O_TRUNC case "append": mode |= os.O_APPEND case "update": // Do nothing: updating in place is the default. case "error": if mode&os.O_CREATE == 0 { return nil, errIfNotExistsAndIfExistsBothError } mode |= os.O_EXCL default: return nil, errs.BadValue{What: "if-exists option", Valid: "truncate, append, update or error", Actual: parse.Quote(opts.IfExists)} } return os.OpenFile(name, mode, fs.FileMode(perm)) } func close(f vals.File) error { return f.Close() } func pipe() (vals.Pipe, error) { r, w, err := os.Pipe() return vals.Pipe{R: r, W: w}, err } type seekOpts struct { Whence string } func (opts *seekOpts) SetDefaultOptions() { opts.Whence = "start" } func seek(opts seekOpts, f vals.File, rawOffset vals.Num) error { offset, err := toInt64(rawOffset, "offset", math.MinInt64, "-2^64") if err != nil { return err } var whence int switch opts.Whence { case "start": whence = io.SeekStart case "current": whence = io.SeekCurrent case "end": whence = io.SeekEnd default: return errs.BadValue{What: "whence", Valid: "start, current or end", Actual: parse.Quote(opts.Whence)} } _, err = f.Seek(offset, whence) return err } func tell(f vals.File) (vals.Num, error) { offset, err := f.Seek(0, io.SeekCurrent) if err != nil { return nil, err } return vals.Int64ToNum(offset), nil } func truncate(name string, rawSize vals.Num) error { size, err := toInt64(rawSize, "size", 0, "0") if err != nil { return err } return os.Truncate(name, size) } func toInt64(n vals.Num, what string, validLow int64, validLowString string) (int64, error) { outOfRange := func() error { return errs.OutOfRange{What: what, ValidLow: validLowString, ValidHigh: "2^64-1", Actual: vals.ToString(n)} } var i int64 switch n := n.(type) { case int: i = int64(n) case *big.Int: if n.IsInt64() { i = n.Int64() } else { return 0, outOfRange() } default: return 0, errs.BadValue{What: what, Valid: "exact integer", Actual: vals.ToString(n), } } if i < validLow { return 0, outOfRange() } return i, nil } elvish-0.21.0/pkg/mods/file/file_test.elvts000066400000000000000000000153001465720375400205770ustar00rootroot00000000000000//each:eval use file //each:in-temp-dir ///////////// # file:open # ///////////// ~> echo haha > out3 var f = (file:open out3) slurp < $f file:close $f ▶ "haha\n" //////////////////// # file:open-output # //////////////////// ## &also-input ## ~> print foo > file var f = (file:open-output &also-input &if-exists=update file) read-bytes 1 < $f print X > $f slurp < $f file:close $f slurp < file ▶ f ▶ o ▶ fXo ## &if-not-exists=create ## ~> var f = (file:open-output new &if-not-exists=create) file:close $f slurp < new ▶ '' ## &if-not-exists=error ## //only-on !windows // Windows has a different error message. // // TODO: Consider normalizing all builtin functions that can return // *fs.PathError so that the error is consistent across platforms. ~> var f = (file:open-output new &if-not-exists=error) Exception: open new: no such file or directory [tty]:1:10-50: var f = (file:open-output new &if-not-exists=error) ## &if-not-exists=error on Windows ## //only-on windows ~> var f = (file:open-output new &if-not-exists=error) Exception: open new: The system cannot find the file specified. [tty]:1:10-50: var f = (file:open-output new &if-not-exists=error) ## default is &if-not-exists=create ## ~> var f = (file:open-output new) file:close $f slurp < new ▶ '' ## invalid &if-not-exists ## ~> var f = (file:open-output new &if-not-exists=bad) Exception: bad value: if-not-exists option must be create or error, but is bad [tty]:1:10-48: var f = (file:open-output new &if-not-exists=bad) ## &if-exists=truncate ## ~> print old-content > old var f = (file:open-output old &if-exists=truncate) print new > $f file:close $f slurp < old ▶ new ## &if-exists=append ## ~> print old-content > old var f = (file:open-output old &if-exists=append) print new > $f file:close $f slurp < old ▶ old-contentnew ## &if-exists=update ## ~> print old-content > old var f = (file:open-output old &if-exists=update) print new > $f file:close $f slurp < old ▶ new-content ## &if-exists=error ## //only-on !windows // Windows has a different error message. ~> print old-content > old var f = (file:open-output old &if-exists=error) Exception: open old: file exists [tty]:2:10-46: var f = (file:open-output old &if-exists=error) ## &if-exists=error on Windows ## //only-on windows // Windows has a different error message. ~> print old-content > old var f = (file:open-output old &if-exists=error) Exception: open old: The file exists. [tty]:2:10-46: var f = (file:open-output old &if-exists=error) ## default is &if-exists=truncate ## ~> print old-content > old var f = (file:open-output old) print new > $f file:close $f slurp < old ▶ new ## invalid &if-exists ## ~> var f = (file:open-output old &if-exists=bad) Exception: bad value: if-exists option must be truncate, append, update or error, but is bad [tty]:1:10-44: var f = (file:open-output old &if-exists=bad) ## &if-exists=error with &if-not-exists=error is an error ## ~> var f = (file:open-output old &if-not-exists=error &if-exists=error) Exception: both &if-not-exists and &if-exists are error [tty]:1:10-67: var f = (file:open-output old &if-not-exists=error &if-exists=error) ## invalid &create-perm ## ~> file:open-output new &create-perm=0o1000 Exception: out of range: create-perm option must be from 0 to 0o777, but is 0o1000 [tty]:1:1-40: file:open-output new &create-perm=0o1000 // TODO: Add test for valid &create-perm ///////////// # file:pipe # ///////////// ~> var p = (file:pipe) echo haha > $p file:close $p[w] slurp < $p file:close $p[r] ▶ "haha\n" ## reading from closed pipe ## ~> var p = (file:pipe) echo foo > $p file:close $p[r] slurp < $p Exception: read |0: file already closed [tty]:4:1-10: slurp < $p ///////////// # file:seek # ///////////// ~> fn test-seek {|offset opts| print 0123456789 > file var f = (file:open file) defer { file:close $f } read-bytes 1 < $f | nop (all) # Using call allows us to test calling file:seek with no &whence call $file:seek~ [$f $offset] $opts read-bytes 1 < $f } ~> test-seek 1 [&] # default is &when=start ▶ 1 ~> test-seek 1 [&whence=start] ▶ 1 ~> test-seek 1 [&whence=current] ▶ 2 ~> test-seek -1 [&whence=end] ▶ 9 ~> test-seek 1 [&whence=bad] Exception: bad value: whence must be start, current or end, but is bad [tty]:7:3-37: call $file:seek~ [$f $offset] $opts [tty]:1:1-25: test-seek 1 [&whence=bad] ~> test-seek 100000000000000000000 [&] Exception: out of range: offset must be from -2^64 to 2^64-1, but is 100000000000000000000 [tty]:7:3-37: call $file:seek~ [$f $offset] $opts [tty]:1:1-35: test-seek 100000000000000000000 [&] ~> test-seek 1.5 [&] Exception: bad value: offset must be exact integer, but is 1.5 [tty]:7:3-37: call $file:seek~ [$f $offset] $opts [tty]:1:1-17: test-seek 1.5 [&] ///////////// # file:tell # ///////////// ~> print 0123456789 > file var f = (file:open file) read-bytes 4 < $f file:tell $f file:close $f ▶ 0123 ▶ (num 4) ///////////////// # file:truncate # ///////////////// ## good case ## ~> use os echo > file100 file:truncate file100 100 put (os:stat file100)[size] ▶ (num 100) // Should also test the case where the argument doesn't fit in an int but does // fit in a int64; but this only happens on 32-bit platforms, and testing it can // consume too much disk. ## bad cases ## ~> file:truncate bad -1 Exception: out of range: size must be from 0 to 2^64-1, but is -1 [tty]:1:1-20: file:truncate bad -1 ~> file:truncate bad 100000000000000000000 Exception: out of range: size must be from 0 to 2^64-1, but is 100000000000000000000 [tty]:1:1-39: file:truncate bad 100000000000000000000 ~> file:truncate bad 1.5 Exception: bad value: size must be exact integer, but is 1.5 [tty]:1:1-21: file:truncate bad 1.5 /////////////// # file:is-tty # /////////////// ## number argument ## ~> file:is-tty 0 ▶ $false ~> file:is-tty (num 0) ▶ $false ## file argument ## ~> var p = (file:pipe) file:is-tty $p[r]; file:is-tty $p[w] file:close $p[r]; file:close $p[w] ▶ $false ▶ $false ## bad arguments ## ~> file:is-tty a Exception: bad value: argument to file:is-tty must be file value or numerical FD, but is a [tty]:1:1-13: file:is-tty a ~> file:is-tty [] Exception: bad value: argument to file:is-tty must be file value or numerical FD, but is [] [tty]:1:1-14: file:is-tty [] ## /dev/null ## //skip-unless-can-open /dev/null ~> file:is-tty 0 < /dev/null ▶ $false ~> file:is-tty (num 0) < /dev/null ▶ $false ## /dev/tty ## //skip-unless-can-open /dev/tty ~> file:is-tty 0 < /dev/tty ▶ $true ~> file:is-tty (num 0) < /dev/tty ▶ $true // TODO: Test with PTY when https://b.elv.sh/1595 is resolved. elvish-0.21.0/pkg/mods/file/file_test.go000066400000000000000000000006471465720375400200570ustar00rootroot00000000000000package file_test import ( "embed" "os" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "skip-unless-can-open", func(t *testing.T, name string) { if !canOpen(name) { t.SkipNow() } }, ) } func canOpen(name string) bool { f, err := os.Open(name) f.Close() return err == nil } elvish-0.21.0/pkg/mods/flag/000077500000000000000000000000001465720375400155355ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/flag/flag.d.elv000066400000000000000000000135131465720375400174030ustar00rootroot00000000000000#//each:eval use flag # Parses flags from `$args` according to the signature of the # `$fn`, using the [Go convention](#go-convention), and calls `$fn`. # # The `$fn` must be a user-defined function (i.e. not a builtin # function or external command). Each option corresponds to a flag; see # [`flag:parse`]() for how the default value affects the behavior of flags. # After parsing, the non-flag arguments are used as function arguments. # # The `&on-parse-error` function can be supplied to control the behavior when # `$args` contains invalid flags or when the number of arguments after parsing # flags doesn't match what `$fn` expects. The function gets an argument that # describes the error condition; the argument should be treated as an opaque # value for now, but will expose more useful fields in future. If # `&on-parse-error` is not supplied, such errors are raised as exceptions. # # Example: # # ```elvish-transcript # ~> use flag # ~> fn f {|&verbose=$false &port=(num 8000) name| put $verbose $port $name } # ~> flag:call $f~ [-verbose -port 80 a.c] # ▶ $true # ▶ (num 80) # ▶ a.c # ~> flag:call $f~ [-unknown-flag] &on-parse-error={|_| echo 'bad usage' } # bad usage # ~> flag:call $f~ [-verbose a b c] &on-parse-error={|_| echo 'bad usage' } # bad usage # ``` # # This function is most useful when creating an Elvish script that accepts # command-line arguments. For example, if a script `a.elv` contains the # following code: # # ```elvish # use flag # fn main { |&verbose=$false &port=(num 8000) name| # ... # } # flag:call $main~ $args &on-parse-error={|_| # echo 'Usage: '(src)[name]' [-verbose] [-port PORT-NUM] name' # exit 1 # } # ``` # # **Note**: This example shows how `&on-parse-error` can be used to print out a # usage text, but it needs to duplicate the names of the options and arguments # accepted by `main`. This is a known limitation and will hopefully be addressed # with a different API in future. # # The script can be used as follows: # # ```elvish-transcript # //skip-test # ~> elvish a.elv -verbose -port 80 foo # ... # ``` # # See also [`flag:parse`](). fn call {|fn args &on-parse-error=$nil| } # Parses flags from `$args` according to the `$specs`, using the [Go # convention](#go-convention). # # The `$args` must be a list of strings containing the command-line arguments # to parse. # # The `$specs` must be a list of flag specs: # # ```elvish # [ # [flag default-value 'description of the flag'] # ... # ] # ``` # # Each flag spec consists of the name of the flag (without the leading `-`), # its default value, and a description. The default value influences the how # the flag gets converted from string: # # - If it is boolean, the flag is a boolean flag (see [Go # convention](#go-convention) for implications). Flag values `0`, `f`, `F`, # `false`, `False` and `FALSE` are converted to `$false`, and `1`, `t`, # `T`, `true`, `True` and `TRUE` to `$true`. Other values are invalid. # # - If it is a string, no conversion is done. # # - If it is a [typed number](language.html#number), the flag value is # converted using [`num`](). # # - If it is a list, the flag value is split at `,` (equivalent to `{|s| put # [(str:split , $s)] }`). # # - If it is none of the above, an exception is thrown. # # On success, this command outputs two values: a map containing the value of # flags defined in `$specs` (whether they appear in `$args` or not), and a list # containing non-flag arguments. # # Example: # # ```elvish-transcript # ~> flag:parse [-v -times 10 foo] [ # [v $false 'Verbose'] # [times (num 1) 'How many times'] # ] # ▶ [×=(num 10) &v=$true] # ▶ [foo] # ~> flag:parse [] [ # [v $false 'Verbose'] # [times (num 1) 'How many times'] # ] # ▶ [×=(num 1) &v=$false] # ▶ [] # ``` # # See also [`flag:call`]() and [`flag:parse-getopt`](). fn parse {|args specs| } # Parses flags from `$args` according to the `$specs`, using the [getopt # convention](#getopt-convention) (see there for the semantics of the options), # and outputs the result. # # The `$args` must be a list of strings containing the command-line arguments # to parse. # # The `$specs` must be a list of flag specs: # # ```elvish # [ # [&short=f &long=flag &arg-optional=$false &arg-required=$false] # ... # ] # ``` # # Each flag spec can contain the following: # # - The short and long form of the flag, without the leading `-` or `--`. The # short form, if non-empty, must be one character. At least one of `&short` # and `&long` must be non-empty. # # - Whether the flag takes an optional argument or a required argument. At # most one of `&arg-optional` and `&arg-required` may be true. # # It is not an error for a flag spec to contain more keys. # # On success, this command outputs two values: a list describing all flags # parsed from `$args`, and a list containing non-flag arguments. The former # list looks like: # # ```elvish # [ # [&spec=... &arg=value &long=$false] # ... # ] # ``` # # Each entry contains the original spec for the flag, its argument, and whether # the flag appeared in its long form. # # Example (some output reformatted for readability): # # ```elvish-transcript # ~> var specs = [ # [&short=v &long=verbose] # [&short=p &long=port &arg-required] # ] # ~> flag:parse-getopt [-v -p 80 foo] $specs # ▶ [[&arg='' &long=$false &spec=[&long=verbose &short=v]] [&arg=80 &long=$false &spec=[&arg-required=$true &long=port &short=p]]] # ▶ [foo] # ~> flag:parse-getopt [--verbose] $specs # ▶ [[&arg='' &long=$true &spec=[&long=verbose &short=v]]] # ▶ [] # ~> flag:parse-getopt [-v] [[&short=v &extra-info=foo]] # extra key in spec # ▶ [[&arg='' &long=$false &spec=[&extra-info=foo &short=v]]] # ▶ [] # ``` # # See also [`flag:parse`]() and [`edit:complete-getopt`](). fn parse-getopt {|args specs &stop-after-double-dash=$true &stop-before-non-flag=$false &long-only=$false| } elvish-0.21.0/pkg/mods/flag/flag.go000066400000000000000000000140141465720375400167750ustar00rootroot00000000000000package flag import ( "errors" "flag" "io" "math/big" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/getopt" ) // Ns is the namespace for the flag: module. var Ns = eval.BuildNsNamed("flag"). AddGoFns(map[string]any{ "call": call, "parse": parse, "parse-getopt": parseGetopt, }).Ns() type callOpts struct { OnParseError eval.Callable } func (*callOpts) SetDefaultOptions() {} // TODO: The &on-parse-error option makes it possible for the user to supply a // custom usage text, but they'll have to essentially duplicate the flag names // and argument names, partially defeating the point of the "call" function. // // Moreover, there isn't a suitable place for descriptions of options in the // lambda signature syntax; as a result, all flags have an empty description and // we can't rely on the default help text available via // [(*flag.FlagSet).PrintDefaults]. func call(fm *eval.Frame, opts callOpts, fn *eval.Closure, argsVal vals.List) error { var args []string err := vals.ScanListToGo(argsVal, &args) if err != nil { return err } fs := newFlagSet("") for i, name := range fn.OptNames { value := fn.OptDefaults[i] err := addFlag(fs, name, value, "") if err != nil { return err } } err = fs.Parse(args) if err != nil { if opts.OnParseError != nil { return opts.OnParseError.Call(fm.Fork(), []any{err}, eval.NoOpts) } return err } m := make(map[string]any) fs.VisitAll(func(f *flag.Flag) { m[f.Name] = f.Value.(flag.Getter).Get() }) err = fn.Call(fm.Fork(), convertStringArgs(fs.Args()), m) if opts.OnParseError != nil { switch err.(type) { case errs.ArityMismatch: return opts.OnParseError.Call(fm.Fork(), []any{err}, eval.NoOpts) } } return err } func convertStringArgs(ss []string) []any { vs := make([]any, len(ss)) for i, s := range ss { vs[i] = s } return vs } func parse(argsVal vals.List, specsVal vals.List) (vals.Map, vals.List, error) { var args []string err := vals.ScanListToGo(argsVal, &args) if err != nil { return nil, nil, err } var specs []vals.List err = vals.ScanListToGo(specsVal, &specs) if err != nil { return nil, nil, err } fs := newFlagSet("") for _, spec := range specs { var ( name string value any description string ) vals.ScanListElementsToGo(spec, &name, &value, &description) err := addFlag(fs, name, value, description) if err != nil { return nil, nil, err } } err = fs.Parse(args) if err != nil { return nil, nil, err } m := vals.EmptyMap fs.VisitAll(func(f *flag.Flag) { m = m.Assoc(f.Name, f.Value.(flag.Getter).Get()) }) return m, vals.MakeListSlice(fs.Args()), nil } func newFlagSet(name string) *flag.FlagSet { fs := flag.NewFlagSet(name, flag.ContinueOnError) fs.SetOutput(io.Discard) return fs } func addFlag(fs *flag.FlagSet, name string, value any, description string) error { switch value := value.(type) { case bool: fs.Bool(name, value, description) case string: fs.String(name, value, description) case int, *big.Int, *big.Rat, float64: fs.Var(&numFlag{value}, name, description) case vals.List: fs.Var(&listFlag{value}, name, description) default: return errs.BadValue{What: "flag default value", Valid: "boolean, number, string or list", Actual: vals.ReprPlain(value)} } return nil } type numFlag struct{ value vals.Num } func (nf *numFlag) String() string { return vals.ToString(nf.value) } func (nf *numFlag) Get() any { return nf.value } func (nf *numFlag) Set(s string) error { return vals.ScanToGo(s, &nf.value) } type listFlag struct{ value vals.List } func (lf *listFlag) String() string { return vals.ToString(lf.value) } func (lf *listFlag) Get() any { return lf.value } func (lf *listFlag) Set(s string) error { lf.value = vals.MakeListSlice(strings.Split(s, ",")) return nil } type specStruct struct { Short rune Long string ArgRequired bool ArgOptional bool } var ( errShortLong = errors.New("at least one of &short and &long must be non-empty") errArgRequiredArgOptional = errors.New("at most one of &arg-required and &arg-optional may be true") ) func (s *specStruct) OptionSpec() (*getopt.OptionSpec, error) { if s.Short == 0 && s.Long == "" { return nil, errShortLong } arity := getopt.NoArgument switch { case s.ArgRequired && s.ArgOptional: return nil, errArgRequiredArgOptional case s.ArgRequired: arity = getopt.RequiredArgument case s.ArgOptional: arity = getopt.OptionalArgument } return &getopt.OptionSpec{Short: s.Short, Long: s.Long, Arity: arity}, nil } type parseGetoptOptions struct { StopAfterDoubleDash bool StopBeforeNonFlag bool LongOnly bool } func (o *parseGetoptOptions) SetDefaultOptions() { o.StopAfterDoubleDash = true } func (o *parseGetoptOptions) Config() getopt.Config { c := getopt.Config(0) if o.StopAfterDoubleDash { c |= getopt.StopAfterDoubleDash } if o.StopBeforeNonFlag { c |= getopt.StopBeforeFirstNonOption } if o.LongOnly { c |= getopt.LongOnly } return c } func parseGetopt(opts parseGetoptOptions, argsVal vals.List, specsVal vals.List) (vals.List, vals.List, error) { var args []string err := vals.ScanListToGo(argsVal, &args) if err != nil { return nil, nil, err } var specMaps []vals.Map err = vals.ScanListToGo(specsVal, &specMaps) if err != nil { return nil, nil, err } specs := make([]*getopt.OptionSpec, len(specMaps)) originalSpecMap := make(map[*getopt.OptionSpec]vals.Map) for i, specMap := range specMaps { var s specStruct vals.ScanMapToGo(specMap, &s) spec, err := s.OptionSpec() if err != nil { return nil, nil, err } specs[i] = spec originalSpecMap[spec] = specMap } flags, nonFlagArgs, err := getopt.Parse(args, specs, opts.Config()) if err != nil { return nil, nil, err } flagsList := vals.EmptyList for _, flag := range flags { flagsList = flagsList.Conj( vals.MakeMap( "spec", originalSpecMap[flag.Spec], "arg", flag.Argument, "long", flag.Long)) } return flagsList, vals.MakeListSlice(nonFlagArgs), nil } elvish-0.21.0/pkg/mods/flag/flag_test.elvts000066400000000000000000000115031465720375400205640ustar00rootroot00000000000000//each:eval use flag ///////////// # flag:call # ///////////// ~> flag:call {|&bool=$false| put $bool } [-bool] ▶ $true ~> flag:call {|&str=''| put $str } [-str foo] ▶ foo ~> flag:call {|&opt=$false arg| put $opt $arg } [-opt foo] ▶ $true ▶ foo ## unsupported default flag value ## ~> flag:call {|&f=$nil| } [-f 1] Exception: bad value: flag default value must be boolean, number, string or list, but is $nil [tty]:1:1-29: flag:call {|&f=$nil| } [-f 1] ## flag parsing error ## ~> flag:call { } [-bad ''] Exception: flag provided but not defined: -bad [tty]:1:1-23: flag:call { } [-bad ''] ## bad flag with &on-parse-error ## // err is opaque, but stringifying it gives us something semi-useful for now ~> flag:call { } [-bad ''] &on-parse-error={|err| echo 'Bad usage' } Bad usage ## wrong number of arguments with &on-parse-error ## ~> flag:call {|&str='' a b| } [-str foo x] &on-parse-error={|err| echo 'Bad usage' } Bad usage ## bad argument type ## ~> flag:call { } [(num 0)] Exception: wrong type: need string, got number [tty]:1:1-23: flag:call { } [(num 0)] // More flag parsing logic is covered in TestParse /////////////// # flag::parse # /////////////// ## different types of flags ## ~> flag:parse [-bool] [[bool $false bool]] ▶ [&bool=$true] ▶ [] ~> flag:parse [-str lorem] [[str '' string]] ▶ [&str=lorem] ▶ [] ~> flag:parse [-num 100] [[num (num 0) number]] ▶ [&num=(num 100)] ▶ [] ~> flag:parse [-list a,b] [[list [] list]] ▶ [&list=[a b]] ▶ [] ## multiple flags, and non-flag arguments ## ~> flag:parse [-v -n foo bar] [[v $false verbose] [n '' name]] ▶ [&n=foo &v=$true] ▶ [bar] ## flag parsing error ## ~> flag:parse [-bad ''] [] Exception: flag provided but not defined: -bad [tty]:1:1-23: flag:parse [-bad ''] [] ## unsupported type for default value ## ~> flag:parse [-map ''] [[map [&] map]] Exception: bad value: flag default value must be boolean, number, string or list, but is [&] [tty]:1:1-36: flag:parse [-map ''] [[map [&] map]] // TODO: Improve these errors to point out where the wrong type occurs ## bad argument list ## ~> flag:parse [(num 0)] [] Exception: wrong type: need string, got number [tty]:1:1-23: flag:parse [(num 0)] [] ## bad spec list ## ~> flag:parse [] [(num 0)] Exception: wrong type: need !!vector.Vector, got number [tty]:1:1-23: flag:parse [] [(num 0)] ///////////////////// # flag:parse-getopt # ///////////////////// ## basic test ## ~> flag:parse-getopt [-v foo] [[&short=v]] ▶ [[&arg='' &long=$false &spec=[&short=v]]] ▶ [foo] ## extra info in spec ## ~> flag:parse-getopt [-v foo] [[&short=v &extra=info]] ▶ [[&arg='' &long=$false &spec=[&extra=info &short=v]]] ▶ [foo] ## spec with &arg-required ## ~> flag:parse-getopt [-p 80 foo] [[&short=p &arg-required]] ▶ [[&arg=80 &long=$false &spec=[&arg-required=$true &short=p]]] ▶ [foo] ## spec with &arg-optional, with argument ## ~> flag:parse-getopt [-i.bak foo] [[&short=i &arg-optional]] ▶ [[&arg=.bak &long=$false &spec=[&arg-optional=$true &short=i]]] ▶ [foo] ## spec with &arg-optional, without argument ## ~> flag:parse-getopt [-i foo] [[&short=i &arg-optional]] ▶ [[&arg='' &long=$false &spec=[&arg-optional=$true &short=i]]] ▶ [foo] ## &stop-after-double-dash on (default) ## ~> flag:parse-getopt [-- -v] [[&short=v]] ▶ [] ▶ [-v] ## &stop-after-double-dash off ## ~> flag:parse-getopt [-- -v] [[&short=v]] &stop-after-double-dash=$false ▶ [[&arg='' &long=$false &spec=[&short=v]]] ▶ [--] ## &stop-before-non-flag off (default) ## ~> flag:parse-getopt [foo -v] [[&short=v]] ▶ [[&arg='' &long=$false &spec=[&short=v]]] ▶ [foo] ## &stop-before-non-flag on ## ~> flag:parse-getopt [foo -v] [[&short=v]] &stop-before-non-flag ▶ [] ▶ [foo -v] ## &long-only off (default) ## ~> flag:parse-getopt [-verbose] [[&long=verbose]] Exception: unknown option -v [tty]:1:1-46: flag:parse-getopt [-verbose] [[&long=verbose]] ## &long-only on ## ~> flag:parse-getopt [-verbose] [[&long=verbose]] &long-only ▶ [[&arg='' &long=$true &spec=[&long=verbose]]] ▶ [] ## neither of &short and &long ## ~> flag:parse-getopt [] [[&]] Exception: at least one of &short and &long must be non-empty [tty]:1:1-26: flag:parse-getopt [] [[&]] ## both &arg-required and &arg-optional ## ~> flag:parse-getopt [] [[&short=x &arg-optional &arg-required]] Exception: at most one of &arg-required and &arg-optional may be true [tty]:1:1-61: flag:parse-getopt [] [[&short=x &arg-optional &arg-required]] ## flag parsing error ## ~> flag:parse-getopt [-x] [] Exception: unknown option -x [tty]:1:1-25: flag:parse-getopt [-x] [] ## bad argument list ## ~> flag:parse-getopt [(num 0)] [] Exception: wrong type: need string, got number [tty]:1:1-30: flag:parse-getopt [(num 0)] [] ## bad spec list ## ~> flag:parse-getopt [] [(num 0)] Exception: wrong type: need !!hashmap.Map, got number [tty]:1:1-30: flag:parse-getopt [] [(num 0)] elvish-0.21.0/pkg/mods/flag/flag_test.go000066400000000000000000000003341465720375400200340ustar00rootroot00000000000000package flag_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts *.elv var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/math/000077500000000000000000000000001465720375400155555ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/math/math.d.elv000066400000000000000000000210171465720375400174410ustar00rootroot00000000000000#//each:eval use math #//only-on amd64 || arm64 # Approximate value of # [`e`](https://en.wikipedia.org/wiki/E_(mathematical_constant)): # 2.718281.... This variable is read-only. var e # Approximate value of [`π`](https://en.wikipedia.org/wiki/Pi): 3.141592.... This # variable is read-only. var pi # 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) # ``` fn abs {|number| } # Outputs the arccosine of `$number`, in radians (not degrees). Examples: # # ```elvish-transcript # ~> math:acos 1 # ▶ (num 0.0) # ~> math:acos 1.00001 # ▶ (num NaN) # ``` fn acos {|number| } # Outputs the inverse hyperbolic cosine of `$number`. Examples: # # ```elvish-transcript # ~> math:acosh 1 # ▶ (num 0.0) # ~> math:acosh 0 # ▶ (num NaN) # ``` fn acosh {|number| } # Outputs the arcsine of `$number`, in radians (not degrees). Examples: # # ```elvish-transcript # ~> math:asin 0 # ▶ (num 0.0) # ~> math:asin 1 # ▶ (num 1.5707963267948966) # ~> math:asin 1.00001 # ▶ (num NaN) # ``` fn asin {|number| } # Outputs the inverse hyperbolic sine of `$number`. Examples: # # ```elvish-transcript # ~> math:asinh 0 # ▶ (num 0.0) # ~> math:asinh inf # ▶ (num +Inf) # ``` fn asinh {|number| } # Outputs the arctangent of `$number`, in radians (not degrees). Examples: # # ```elvish-transcript # ~> math:atan 0 # ▶ (num 0.0) # ~> math:atan +inf # ▶ (num 1.5707963267948966) # ``` fn atan {|number| } # Outputs the arc tangent of *y*/*x* in radians, using the signs of the two to # determine the quadrant of the return value. Examples: # # ```elvish-transcript # ~> math:atan2 0 0 # ▶ (num 0.0) # ~> math:atan2 1 1 # ▶ (num 0.7853981633974483) # ~> math:atan2 -1 -1 # ▶ (num -2.356194490192345) # ``` fn atan2 {|y x| } # Outputs the inverse hyperbolic tangent of `$number`. Examples: # # ```elvish-transcript # ~> math:atanh 0 # ▶ (num 0.0) # ~> math:atanh 1 # ▶ (num +Inf) # ``` fn atanh {|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:ceil 1 # ▶ (num 1) # ~> math:ceil 3/2 # ▶ (num 2) # ~> math:ceil -3/2 # ▶ (num -1) # ~> math:ceil 1.1 # ▶ (num 2.0) # ~> math:ceil -1.1 # ▶ (num -1.0) # ``` fn ceil {|number| } # Computes the cosine of `$number` in units of radians (not degrees). # Examples: # # ```elvish-transcript # ~> math:cos 0 # ▶ (num 1.0) # ~> math:cos 3.14159265 # ▶ (num -1.0) # ``` fn cos {|number| } # Computes the hyperbolic cosine of `$number`. Example: # # ```elvish-transcript # ~> math:cosh 0 # ▶ (num 1.0) # ``` fn cosh {|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) # ``` fn floor {|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 # ``` fn is-inf {|&sign=0 number| } # Tests whether the number is a NaN (not-a-number). # # ```elvish-transcript # ~> math:is-nan 123 # ▶ $false # ~> math:is-nan (num inf) # ▶ $false # ~> math:is-nan (num nan) # ▶ $true # ``` fn is-nan {|number| } # Computes the natural (base *e*) logarithm of `$number`. Examples: # # ```elvish-transcript # ~> math:log 1.0 # ▶ (num 0.0) # ~> math:log -2.3 # ▶ (num NaN) # ``` fn log {|number| } # Computes the base 10 logarithm of `$number`. Examples: # # ```elvish-transcript # ~> math:log10 100.0 # ▶ (num 2.0) # ~> math:log10 -1.7 # ▶ (num NaN) # ``` fn log10 {|number| } # Computes the base 2 logarithm of `$number`. Examples: # # ```elvish-transcript # ~> math:log2 8 # ▶ (num 3.0) # ~> math:log2 -5.3 # ▶ (num NaN) # ``` fn log2 {|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) # ``` fn max {|@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]:1:1-8: math:min # ~> math:min 3 5 2 # ▶ (num 2) # ~> math:min 1/2 1/3 2/3 # ▶ (num 1/3) # ``` fn min {|@number| } # 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) # ``` fn pow {|base exponent| } # 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) # ``` fn round {|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) # ``` fn round-to-even {|number| } # Computes the sine of `$number` in units of radians (not degrees). Examples: # # ```elvish-transcript # ~> math:sin 0 # ▶ (num 0.0) # ~> math:sin 3.14159265 # ▶ (num 3.5897930298416118e-09) # ``` fn sin {|number| } # Computes the hyperbolic sine of `$number`. Example: # # ```elvish-transcript # ~> math:sinh 0 # ▶ (num 0.0) # ``` fn sinh {|number| } # Computes the square-root of `$number`. Examples: # # ```elvish-transcript # ~> math:sqrt 0 # ▶ (num 0.0) # ~> math:sqrt 4 # ▶ (num 2.0) # ~> math:sqrt -4 # ▶ (num NaN) # ``` fn sqrt {|number| } # Computes the tangent of `$number` in units of radians (not degrees). Examples: # # ```elvish-transcript # ~> math:tan 0 # ▶ (num 0.0) # ~> math:tan 3.14159265 # ▶ (num -0.0000000035897930298416118) # ``` fn tan {|number| } # Computes the hyperbolic tangent of `$number`. Example: # # ```elvish-transcript # ~> math:tanh 0 # ▶ (num 0.0) # ``` fn tanh {|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) # ``` fn trunc {|number| } elvish-0.21.0/pkg/mods/math/math.go000066400000000000000000000144171465720375400170440ustar00rootroot00000000000000// 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]any{ "abs": abs, "acos": math.Acos, "acosh": math.Acosh, "asin": math.Asin, "asinh": math.Asinh, "atan": math.Atan, "atan2": math.Atan2, "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() 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") } } 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) }) } 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()) }) } 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 } func isNaN(n vals.Num) bool { if f, ok := n.(float64); ok { return math.IsNaN(f) } return false } 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") } } 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") } } 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 } } 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) }) } 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) }) } 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.21.0/pkg/mods/math/math_test.elvts000066400000000000000000000170351465720375400206320ustar00rootroot00000000000000//each:eval use math //////////// # math:abs # //////////// ~> math:abs 2 ▶ (num 2) ~> math:abs -2 ▶ (num 2) ~> math:abs -2147483648 # -2^31 ▶ (num 2147483648) ~> math:abs -9223372036854775808 # -2^63 ▶ (num 9223372036854775808) ~> math:abs 100000000000000000000 ▶ (num 100000000000000000000) ~> math:abs -100000000000000000000 ▶ (num 100000000000000000000) ~> math:abs -1/2 ▶ (num 1/2) ~> math:abs 1/2 ▶ (num 1/2) ~> math:abs 2.1 ▶ (num 2.1) ~> math:abs -2.1 ▶ (num 2.1) ///////////// # math:ceil # ///////////// ~> math:ceil 2 ▶ (num 2) ~> math:ceil 100000000000000000000 ▶ (num 100000000000000000000) ~> math:ceil 3/2 ▶ (num 2) ~> math:ceil -3/2 ▶ (num -1) ~> math:ceil 2.1 ▶ (num 3.0) ~> math:ceil -2.1 ▶ (num -2.0) ////////////// # math:floor # ////////////// ~> math:floor 2 ▶ (num 2) ~> math:floor 100000000000000000000 ▶ (num 100000000000000000000) ~> math:floor 3/2 ▶ (num 1) ~> math:floor -3/2 ▶ (num -2) ~> math:floor 2.1 ▶ (num 2.0) ~> math:floor -2.1 ▶ (num -3.0) ////////////// # math:round # ////////////// ~> math:round 2 ▶ (num 2) ~> math:round 100000000000000000000 ▶ (num 100000000000000000000) ~> 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.1 ▶ (num 2.0) ~> math:round 2.5 ▶ (num 3.0) ////////////////////// # math:round-to-even # ////////////////////// ~> math:round-to-even 2 ▶ (num 2) ~> math:round-to-even 100000000000000000000 ▶ (num 100000000000000000000) ~> math:round-to-even 1/3 ▶ (num 0) ~> math:round-to-even 2/3 ▶ (num 1) ~> math:round-to-even -1/3 ▶ (num 0) ~> math:round-to-even -2/3 ▶ (num -1) ~> math:round-to-even 2.5 ▶ (num 2.0) ~> math:round-to-even -2.5 ▶ (num -2.0) ~> 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 7/2 ▶ (num 4) ~> 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 -7/2 ▶ (num -4) ////////////// # math:trunc # ////////////// ~> math:trunc 2 ▶ (num 2) ~> math:trunc 100000000000000000000 ▶ (num 100000000000000000000) ~> math:trunc 3/2 ▶ (num 1) ~> math:trunc -3/2 ▶ (num -1) ~> math:trunc 2.1 ▶ (num 2.0) ~> math:trunc -2.1 ▶ (num -2.0) ~> math:trunc (num Inf) ▶ (num +Inf) ~> math:trunc (num NaN) ▶ (num NaN) /////////////// # math:is-inf # /////////////// ~> math:is-inf 1.3 ▶ $false ~> math:is-inf &sign=0 inf ▶ $true ~> math:is-inf &sign=1 inf ▶ $true ~> math:is-inf &sign=-1 -inf ▶ $true ~> math:is-inf &sign=1 -inf ▶ $false ~> math:is-inf -inf ▶ $true ~> math:is-inf nan ▶ $false ~> math:is-inf 1 ▶ $false ~> math:is-inf 100000000000000000000 ▶ $false ~> math:is-inf 1/2 ▶ $false /////////////// # math:is-nan # /////////////// ~> math:is-nan 1.3 ▶ $false ~> math:is-nan inf ▶ $false ~> math:is-nan nan ▶ $true ~> math:is-nan 1 ▶ $false ~> math:is-nan 100000000000000000000 ▶ $false ~> math:is-nan 1/2 ▶ $false //////////// # math:max # //////////// ~> math:max Exception: arity mismatch: arguments must be 1 or more values, but is 0 values [tty]:1:1-8: math:max ~> math:max 42 ▶ (num 42) ~> math:max -3 3 10 -4 ▶ (num 10) ~> math:max 2 10 100000000000000000000 ▶ (num 100000000000000000000) ~> math:max 100000000000000000001 100000000000000000002 100000000000000000000 ▶ (num 100000000000000000002) ~> math:max 1/2 1/3 2/3 ▶ (num 2/3) ~> math:max 1.0 2.0 ▶ (num 2.0) ~> math:max 3 NaN 5 ▶ (num NaN) //////////// # math:min # //////////// ~> math:min Exception: arity mismatch: arguments must be 1 or more values, but is 0 values [tty]:1:1-8: math:min ~> math:min 42 ▶ (num 42) ~> math:min -3 3 10 -4 ▶ (num -4) ~> math:min 2 10 100000000000000000000 ▶ (num 2) ~> math:min 100000000000000000001 100000000000000000002 100000000000000000000 ▶ (num 100000000000000000000) ~> math:min 1/2 1/3 2/3 ▶ (num 1/3) ~> math:min 1.0 2.0 ▶ (num 1.0) ~> math:min 3 NaN 5 ▶ (num NaN) //////////// # math:pow # //////////// ## base is int, exp is int ## ~> math:pow 2 0 ▶ (num 1) ~> math:pow 2 1 ▶ (num 2) ~> math:pow 2 -1 ▶ (num 1/2) ~> math:pow 2 3 ▶ (num 8) ~> math:pow 2 -3 ▶ (num 1/8) ## base is *big.Rat, exp is int ## ~> math:pow 2/3 0 ▶ (num 1) ~> math:pow 2/3 1 ▶ (num 2/3) ~> math:pow 2/3 -1 ▶ (num 3/2) ~> math:pow 2/3 3 ▶ (num 8/27) ~> math:pow 2/3 -3 ▶ (num 27/8) ## exp is *big.Rat ## ~> math:pow 4 1/2 ▶ (num 2.0) ## exp is float64 ## ~> math:pow 2 2.0 ▶ (num 4.0) ~> math:pow 1/2 2.0 ▶ (num 0.25) ## base is float64 ## ~> math:pow 2.0 2 ▶ (num 4.0) //////////// # $math:pi # //////////// // The exact values of some floating-point numbers can vary slightly by // architecture (in particular s370x), so test them at a lower precision. Use // %.4f as a convention. ~> printf "%.4f\n" $math:pi 3.1416 /////////// # $math:e # /////////// ~> printf "%.4f\n" $math:e 2.7183 //////////// # math:log # //////////// ~> math:log $math:e ▶ (num 1.0) ~> math:log 1 ▶ (num 0.0) ~> math:log 0 ▶ (num -Inf) ~> math:log -1 ▶ (num NaN) ////////////// # math:log10 # ////////////// ~> math:log10 10.0 ▶ (num 1.0) ~> math:log10 100.0 ▶ (num 2.0) ~> math:log10 1 ▶ (num 0.0) ~> math:log10 0 ▶ (num -Inf) ~> math:log10 -1 ▶ (num NaN) ///////////// # math:log2 # ///////////// ~> math:log2 8 ▶ (num 3.0) ~> math:log2 1024.0 ▶ (num 10.0) ~> math:log2 1 ▶ (num 0.0) ~> math:log2 0 ▶ (num -Inf) ~> math:log2 -1 ▶ (num NaN) //////////// # math:cos # //////////// ~> math:cos 0 ▶ (num 1.0) ~> math:cos 1 | printf "%.4f\n" (one) 0.5403 ~> math:cos $math:pi ▶ (num -1.0) ///////////// # math:cosh # ///////////// ~> math:cosh 0 ▶ (num 1.0) ~> math:cosh inf ▶ (num +Inf) ~> math:cosh nan ▶ (num NaN) //////////// # math:sin # //////////// ~> math:sin 0 ▶ (num 0.0) ~> math:sin 1 | printf "%.4f\n" (one) 0.8415 ~> math:sin $math:pi | printf "%.4f\n" (one) 0.0000 ///////////// # math:sinh # ///////////// ~> math:sinh 0 ▶ (num 0.0) ~> math:sinh inf ▶ (num +Inf) ~> math:sinh nan ▶ (num NaN) //////////// # math:tan # //////////// ~> math:tan 0 ▶ (num 0.0) ~> math:tan 1 | printf "%.4f\n" (one) 1.5574 ~> math:tan $math:pi | printf "%.4f\n" (one) -0.0000 ///////////// # math:tanh # ///////////// ~> math:tanh 0 ▶ (num 0.0) ~> math:tanh inf ▶ (num 1.0) ~> math:tanh nan ▶ (num NaN) ///////////// # math:sqrt # ///////////// ~> math:sqrt 0 ▶ (num 0.0) ~> math:sqrt 4 ▶ (num 2.0) ~> math:sqrt -4 ▶ (num NaN) ///////////// # math:acos # ///////////// ~> math:acos 0 | printf "%.4f\n" (one) 1.5708 ~> math:acos 1 ▶ (num 0.0) ~> math:acos 1.00001 ▶ (num NaN) ///////////// # math:asin # ///////////// ~> math:asin 0 ▶ (num 0.0) ~> math:asin 1 | printf "%.4f\n" (one) 1.5708 ~> math:asin 1.00001 ▶ (num NaN) ///////////// # math:atan # ///////////// ~> math:atan 0 ▶ (num 0.0) ~> math:atan 1 | printf "%.4f\n" (one) 0.7854 ~> math:atan inf | printf "%.4f\n" (one) 1.5708 ////////////// # math:atan2 # ////////////// ~> math:atan2 0 0 ▶ (num 0.0) ~> math:atan2 1 1 ▶ (num 0.7853981633974483) ~> math:atan2 -1 -1 ▶ (num -2.356194490192345) ////////////// # math:acosh # ////////////// ~> math:acosh 0 ▶ (num NaN) ~> math:acosh 1 ▶ (num 0.0) ~> math:acosh nan ▶ (num NaN) ////////////// # math:asinh # ////////////// ~> math:asinh 0 ▶ (num 0.0) ~> math:asinh 1 | printf "%.4f\n" (one) 0.8814 ~> math:asinh inf ▶ (num +Inf) ////////////// # math:atanh # ////////////// ~> math:atanh 0 ▶ (num 0.0) ~> math:atanh 1 ▶ (num +Inf) elvish-0.21.0/pkg/mods/math/math_test.go000066400000000000000000000003341465720375400200740ustar00rootroot00000000000000package math_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts *.elv var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/md/000077500000000000000000000000001465720375400152245ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/md/md.d.elv000066400000000000000000000010531465720375400165550ustar00rootroot00000000000000#//each:eval use md #doc:added-in 0.21 # Renders `$markdown` in the terminal. # # The `&width` option specifies the width to wrap the output to. If it is 0 (the # default) or negative, `show` queries the terminal width of the standard output # and use it as the width, falling back to 80 if the query fails (for example # when the standard output is not a terminal). # # Examples: # # ```elvish-transcript # ~> md:show "#h1 heading\n- List\n- Item" # #h1 heading # # • List # # • Item # ``` # # See also [`doc:show`](). fn show {|&width=0| markdown} elvish-0.21.0/pkg/mods/md/md.go000066400000000000000000000013671465720375400161620ustar00rootroot00000000000000// Package md exposes functionality from src.elv.sh/pkg/md. package md import ( "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/md" "src.elv.sh/pkg/sys" ) // Ns is the namespace for the md: module. var Ns = eval.BuildNsNamed("md"). AddGoFns(map[string]any{ "show": show, }).Ns() type showOpts struct { Width int } func (*showOpts) SetDefaultOptions() {} func show(fm *eval.Frame, opts showOpts, markdown string) error { width := opts.Width if width <= 0 { _, width = sys.WinSize(fm.Port(1).File) if width <= 0 { width = 80 } } codec := &md.TTYCodec{ Width: width, HighlightCodeBlock: elvdoc.HighlightCodeBlock, } _, err := fm.ByteOutput().WriteString(md.RenderString(markdown, codec)) return err } elvish-0.21.0/pkg/mods/md/md_test.elvts000066400000000000000000000014411465720375400177420ustar00rootroot00000000000000//each:eval use md /////////// # md:show # /////////// // Transcript tests are not run with a real terminal connected to the output, so // the width will fall back to 80. ~> md:show 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ~> md:show "#h1 heading\n- List\n- Item" #h1 heading • List • Item ## explicit &width ## ~> md:show &width=40 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. elvish-0.21.0/pkg/mods/md/md_test.go000066400000000000000000000003321465720375400172100ustar00rootroot00000000000000package md_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts *.elv var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/mods.go000066400000000000000000000023571465720375400161240ustar00rootroot00000000000000// Package mods collects standard library modules. package mods import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/mods/doc" "src.elv.sh/pkg/mods/epm" "src.elv.sh/pkg/mods/file" "src.elv.sh/pkg/mods/flag" "src.elv.sh/pkg/mods/math" "src.elv.sh/pkg/mods/md" "src.elv.sh/pkg/mods/os" "src.elv.sh/pkg/mods/path" "src.elv.sh/pkg/mods/platform" "src.elv.sh/pkg/mods/re" readline_binding "src.elv.sh/pkg/mods/readline-binding" "src.elv.sh/pkg/mods/runtime" "src.elv.sh/pkg/mods/str" "src.elv.sh/pkg/mods/unix" ) // AddTo adds all standard library modules to the Evaler. // // Some modules (the runtime module for now) may rely on properties set on the // Evaler, so any mutations afterwards may not be properly reflected. func AddTo(ev *eval.Evaler) { ev.AddModule("runtime", runtime.Ns(ev)) 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.AddModule("flag", flag.Ns) ev.AddModule("doc", doc.Ns) ev.AddModule("os", os.Ns) ev.AddModule("md", md.Ns) if unix.ExposeUnixNs { ev.AddModule("unix", unix.Ns) } ev.BundledModules["epm"] = epm.Code ev.BundledModules["readline-binding"] = readline_binding.Code } elvish-0.21.0/pkg/mods/os/000077500000000000000000000000001465720375400152455ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/os/os.d.elv000066400000000000000000000204741465720375400166270ustar00rootroot00000000000000# OS-specific path to the "null" device: `/dev/null` on Unix and `NUL` on # Windows. var dev-null # OS-specific path to the terminal device: `/dev/tty` on Unix and `CON` on # Windows. var dev-tty #doc:show-unstable # Reports whether an exception is caused by the fact that a file or directory # already exists. fn -is-exist {|exc| } #doc:show-unstable # Reports whether an exception is caused by the fact that a file or directory # does not exist. fn -is-not-exist {|exc| } # Creates a new directory with the specified name and permission (before umask). fn mkdir {|&perm=0o755 path| } #doc:added-in 0.21 # Creates a new directory at the named path along with any necessary parents. # The permission bits is used for all new directories to create. If the named # path is already a directory, does nothing. fn mkdir-all {|&perm=0o755 path| } #doc:added-in 0.21 # Creates `$newname` as a symbolic link to `$oldname`. # # It is not an error if `$oldname` doesn't exist. However, on Windows, doing # this will create `$newname` as a file symlink, so if `$oldname` is later # created as a directory it will not work. fn symlink {|oldname newname| } # Removes the file or empty directory at `path`. # # If the path does not exist, this command throws an exception that can be # tested with [`os:-is-not-exist`](). fn remove {|path| } # Removes the named file or directory at `path` and, in the latter case, any # children it contains. It removes everything it can, but returns the first # error it encounters. # # If the path does not exist, this command returns silently without throwing an # exception. fn remove-all {|path| } #doc:added-in 0.21 # Renames file at `$oldpath` to `$newpath`. If `$newpath` already exists and is # a file, it will get replaced. # # OS-specific restrictions may apply when `$oldpath` and `$newpath` are in # different directories. On non-Unix platforms, this is not an atomic operation # even within the same directory. fn rename {|oldpath newpath| } # 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. # # ```elvish-transcript # ~> mkdir bin # ~> ln -s bin sbin # ~> os:eval-symlinks ./sbin/a_command # ▶ bin/a_command # ``` fn eval-symlinks {|path| } # Describes the file at `path` by writing a map with the following fields: # # - `name`: The base name of the file. # # - `size`: Length in bytes for regular files; system-dependent for others. # # - `type`: One of `regular`, `dir`, `symlink`, `named-pipe`, `socket`, # `device`, `char-device` and `irregular`. # # - `perm`: Permission bits of the file following Unix's convention. # # See [numeric notation of Unix # permission](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) # for a description of the convention, but note that Elvish prints this number # in decimal; use [`printf`]() with `%o` to print it as an octal number. # # - `special-modes`: A list containing one or more of `setuid`, `setgid` and # `sticky` to indicate the presence of any special mode. # # - `sys`: System-dependent information: # # - On Unix, a map that corresponds 1:1 to the `stat_t` struct, except that # timestamp fields are not exposed yet. # # - On Windows, a map that currently contains just one field, # `file-attributes`, which is a list describing which [file attribute # fields](https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants) # are set. For example, if `FILE_ATTRIBUTE_READONLY` is set, the list # contains `readonly`. # # See [`follow-symlink`](#follow-symlink) for an explanation of the option. # # Examples: # # ```elvish-transcript # ~> echo content > regular # ~> os:stat regular # ▶ [&name=regular &perm=(num 420) &size=(num 8) &special-modes=[] &sys=[&...] &type=regular] # ~> mkdir dir # ~> os:stat dir # ▶ [&name=dir &perm=(num 493) &size=(num 96) &special-modes=[] &sys=[&...] &type=dir] # ~> ln -s dir symlink # ~> os:stat symlink # ▶ [&name=symlink &perm=(num 493) &size=(num 3) &special-modes=[] &sys=[&...] &type=symlink] # ~> os:stat &follow-symlink symlink # ▶ [&name=symlink &perm=(num 493) &size=(num 96) &special-modes=[] &sys=[&...] &type=dir] # ``` fn stat {|&follow-symlink=$false path| } # Reports whether a file is known to exist at `path`. # # See [`follow-symlink`](#follow-symlink) for an explanation of the option. fn exists {|&follow-symlink=$false path| } # Reports whether a directory exists at the path. # # See [`follow-symlink`](#follow-symlink) for an explanation of the option. # # ```elvish-transcript # ~> touch not-a-dir # ~> os:is-dir not-a-dir # ▶ false # ~> os:is-dir /tmp # ▶ true # ``` # # See also [`os:is-regular`](). fn is-dir {|&follow-symlink=$false path| } # Reports whether a regular file exists at the path. # # **Note:** Some other languages call this functionality something like # `is-file`; that name is not chosen because the name "file" also includes # things like directories and device files. # # ```elvish-transcript # ~> touch not-a-dir # ~> os:is-regular not-a-dir # ▶ true # ~> os:is-regular /tmp # ▶ false # ``` # # See also [`os:is-dir`](). fn is-regular {|&follow-symlink=$false path| } # Changes the mode of the file at `$path` to have permission bits set to `$perm` # and special modes set to `$special-modes`. # # The permission bits follow the [numeric notation of Unix # permission](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation), # but note Elvish requires a `0o` prefix for octal numbers (unprefixed numbers # like `444` are interpreted as decimal instead). On Windows, only the `0o200` # bit (owner writable) is used; clearing it makes the file read-only. All other # bits are ignored. # # The special modes should be specified as a list, with elements being one of # `setuid`, `setgid` or `sticky`. # # If the file is a symbolic link, this command always on the link's target. # # Example: # # ```elvish-transcript # ~> touch file # ~> printf "%o %v\n" (os:stat file)[perm special-modes] # 644 [] # ~> os:chmod &special-modes=[sticky] 0o600 file # ~> printf "%o %v\n" (os:stat file)[perm special-modes] # 600 [sticky] # ``` fn chmod {|&special-modes=[] perm path| } # 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 # ~> os:temp-dir # ▶ /tmp/elvish-RANDOMSTR # ~> os:temp-dir x- # ▶ /tmp/x-RANDOMSTR # ~> os:temp-dir 'x-*.y' # ▶ /tmp/x-RANDOMSTR.y # ~> os:temp-dir &dir=. # ▶ elvish-RANDOMSTR # ~> os:temp-dir &dir=/some/dir # ▶ /some/dir/elvish-RANDOMSTR # ``` fn temp-dir {|&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 # ~> var f = (os:temp-file) # ~> put $f[name] # ▶ /tmp/elvish-RANDOMSTR # ~> echo hello > $f # ~> cat $f[name] # hello # ~> var f = (os:temp-file x-) # ~> put $f[name] # ▶ /tmp/x-RANDOMSTR # ~> var f = (os:temp-file 'x-*.y') # ~> put $f[name] # ▶ /tmp/x-RANDOMSTR.y # ~> var f = (os:temp-file &dir=.) # ~> put $f[name] # ▶ elvish-RANDOMSTR # ~> var f = (os:temp-file &dir=/some/dir) # ~> put $f[name] # ▶ /some/dir/elvish-RANDOMSTR # ``` fn temp-file {|&dir='' pattern?| } elvish-0.21.0/pkg/mods/os/os.go000066400000000000000000000111251465720375400162150ustar00rootroot00000000000000// Package os exposes functionality from Go's os package as an Elvish module. package os import ( "io/fs" "os" "path/filepath" "strconv" "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 Elvish namespace for this module. var Ns = eval.BuildNsNamed("os"). AddVars(map[string]vars.Var{ "dev-null": vars.NewReadOnly(os.DevNull), "dev-tty": vars.NewReadOnly(DevTTY), }). AddGoFns(map[string]any{ "-is-exist": isExist, "-is-not-exist": isNotExist, // File CRUD. "mkdir": mkdir, "mkdir-all": mkdirAll, "symlink": os.Symlink, "remove": remove, "remove-all": removeAll, "rename": os.Rename, "chmod": chmod, // File query. "stat": stat, "exists": exists, "is-dir": IsDir, "is-regular": IsRegular, "eval-symlinks": filepath.EvalSymlinks, // Temp file/dir. "temp-dir": TempDir, "temp-file": TempFile, }).Ns() // Wraps [os.IsNotExist] to operate on Exception values. func isExist(e eval.Exception) bool { return os.IsExist(e.Reason()) } // Wraps [os.IsNotExist] to operate on Exception values. func isNotExist(e eval.Exception) bool { return os.IsNotExist(e.Reason()) } type mkdirOpts struct{ Perm int } func (opts *mkdirOpts) SetDefaultOptions() { opts.Perm = 0755 } func mkdir(opts mkdirOpts, path string) error { return os.Mkdir(path, os.FileMode(opts.Perm)) } func mkdirAll(opts mkdirOpts, path string) error { return os.MkdirAll(path, os.FileMode(opts.Perm)) } // ErrEmptyPath is thrown by remove and remove-all when given an empty path. var ErrEmptyPath = errs.BadValue{ What: "path", Valid: "non-empty string", Actual: "empty string"} // Wraps [os.Remove] to reject empty paths. func remove(path string) error { if path == "" { return ErrEmptyPath } return os.Remove(path) } // Wraps [os.RemoveAll] to reject empty paths, and resolve relative paths to // absolute paths first. The latter is necessary since the working directory // could be changed while [os.RemoveAll] is running. func removeAll(path string) error { if path == "" { return ErrEmptyPath } if !filepath.IsAbs(path) { absPath, err := filepath.Abs(path) if err != nil { return err } path = absPath } return os.RemoveAll(path) } type chmodOpts struct { SpecialModes any } func (*chmodOpts) SetDefaultOptions() {} func chmod(opts chmodOpts, perm int, path string) error { if perm < 0 || perm > 0x777 { return errs.OutOfRange{What: "permission bits", ValidLow: "0", ValidHigh: "0o777", Actual: strconv.Itoa(perm)} } mode := fs.FileMode(perm) if opts.SpecialModes != nil { special, err := specialModesFromIterable(opts.SpecialModes) if err != nil { return err } mode |= special } return os.Chmod(path, mode) } type statOpts struct{ FollowSymlink bool } func (opts *statOpts) SetDefaultOptions() {} func stat(opts statOpts, path string) (vals.Map, error) { fi, err := statOrLstat(path, opts.FollowSymlink) if err != nil { return nil, err } return statMap(fi), nil } func exists(opts statOpts, path string) bool { _, err := statOrLstat(path, opts.FollowSymlink) return err == nil } // IsDir is exported so that the implementation may be shared by the path: // module. func IsDir(opts statOpts, path string) bool { fi, err := statOrLstat(path, opts.FollowSymlink) return err == nil && fi.Mode().IsDir() } // IsRegular is exported so that the implementation may be shared by the path: // module. func IsRegular(opts statOpts, path string) bool { fi, err := statOrLstat(path, opts.FollowSymlink) return err == nil && fi.Mode().IsRegular() } func statOrLstat(path string, followSymlink bool) (os.FileInfo, error) { if followSymlink { return os.Stat(path) } else { return os.Lstat(path) } } type mktempOpt struct{ Dir string } func (o *mktempOpt) SetDefaultOptions() {} // TempDir is exported so that the implementation may be shared by the path: // module. func TempDir(opts mktempOpt, args ...string) (string, error) { pattern, err := optionalTempPattern(args) if err != nil { return "", err } return os.MkdirTemp(opts.Dir, pattern) } // TempFile is exported so that the implementation may be shared by the path: // module. func TempFile(opts mktempOpt, args ...string) (*os.File, error) { pattern, err := optionalTempPattern(args) if err != nil { return nil, err } return os.CreateTemp(opts.Dir, pattern) } func optionalTempPattern(args []string) (string, error) { switch len(args) { case 0: return "elvish-*", nil case 1: return args[0], nil default: return "", errs.ArityMismatch{What: "arguments", ValidLow: 0, ValidHigh: 1, Actual: len(args)} } } elvish-0.21.0/pkg/mods/os/os_test.elvts000066400000000000000000000234571465720375400200170ustar00rootroot00000000000000//each:eval use os //each:in-temp-dir //////////// # os:mkdir # //////////// ~> os:mkdir d os:is-dir d ▶ $true ## error when directory already exists ## ~> os:mkdir d try { os:mkdir d } catch e { os:-is-exist $e } ▶ $true ## &perm ## //only-on !windows //umask 0 // TODO: Re-enable this after os:mkdir respects &perm on Windows. ~> os:mkdir &perm=0o555 d555 printf "%O\n" (os:stat d555)[perm] 0o555 ## &perm on Unix ## //only-on unix //umask 0 // Windows only supports 0o555 and 0o777; test other values on Unix only. ~> os:mkdir &perm=0o400 d400 printf "%O\n" (os:stat d400)[perm] 0o400 //////////////// # os:mkdir-all # //////////////// ~> os:mkdir-all a/b os:is-dir a os:is-dir a/b ▶ $true ▶ $true ## no error if the directory already exists ## ~> os:mkdir-all a/b ~> os:mkdir-all a/b ## &perm ## //only-on !windows //umask 0 ~> os:mkdir-all &perm=0o700 a/b printf "%O\n" (os:stat a)[perm] printf "%O\n" (os:stat a/b)[perm] 0o700 0o700 ////////////// # os:symlink # ////////////// //only-if-can-create-symlink ~> echo foo > regular ~> os:symlink regular symlink ~> slurp < symlink ▶ "foo\n" ~> os:eval-symlinks symlink ▶ regular ///////////// # os:remove # ///////////// ~> echo > f; os:exists f os:remove f; os:exists f ▶ $true ▶ $false ~> os:mkdir d; os:exists d os:remove d; os:exists d ▶ $true ▶ $false ## can't remove non-empty directory (Unix) ## //only-on unix ~> os:mkdir d; echo > d/file os:remove d Exception: remove d: directory not empty [tty]:2:1-11: os:remove d ## can't remove non-empty directory (Windows) ## //only-on windows // Windows has a different error message. ~> os:mkdir d; echo > d/file os:remove d Exception: remove d: The directory is not empty. [tty]:2:1-11: os:remove d ## can't remove non-existent file ## ~> try { os:remove d } catch e { os:-is-not-exist $e } ▶ $true ## doesn't take empty string ## ~> os:remove "" Exception: bad value: path must be non-empty string, but is empty string [tty]:1:1-12: os:remove "" ///////////////// # os:remove-all # ///////////////// ## relative path ## ~> os:mkdir d; echo > d/file os:remove-all d; os:exists d ▶ $false ## absolute path ## ~> os:mkdir d; echo > d/file os:remove-all $pwd/d; os:exists d ▶ $false ## removing non-existent file is not an error ## ~> os:remove-all d ## doesn't take empty string ## ~> os:remove-all "" Exception: bad value: path must be non-empty string, but is empty string [tty]:1:1-16: os:remove-all "" ///////////// # os:rename # ///////////// ~> echo > old os:exists old os:exists new ▶ $true ▶ $false ~> os:rename old new os:exists old os:exists new ▶ $false ▶ $true //////////// # os:chmod # //////////// ~> os:mkdir d ~> os:chmod 0o555 d printf "%O\n" (os:stat d)[perm] 0o555 ~> os:chmod 0o777 d printf "%O\n" (os:stat d)[perm] 0o777 ## Unix ## //only-on unix // Windows only supports 0o555 (for read-only files) and 0o777 (for non-readonly // files) in the perm bits, and no special modes. Test more perm bits and // special modes on Unix only. ~> os:mkdir d ~> os:chmod &special-modes=[setuid setgid sticky] 0o400 d put (printf "%O" (os:stat d)[perm]) put (os:stat d)[special-modes] ▶ 0o400 ▶ [setuid setgid sticky] ## invalid arguments ## ~> os:chmod -1 d Exception: out of range: permission bits must be from 0 to 0o777, but is -1 [tty]:1:1-13: os:chmod -1 d // TODO: This error should be more informative and point out that it is the // special modes that should be iterable ~> os:chmod &special-modes=(num 0) 0 d Exception: cannot iterate number [tty]:1:1-35: os:chmod &special-modes=(num 0) 0 d ~> os:chmod &special-modes=[bad] 0 d Exception: bad value: special mode must be setuid, setgid or sticky, but is bad [tty]:1:1-33: os:chmod &special-modes=[bad] 0 d /////////// # os:stat # /////////// // Test basic fields common to all platforms. The perm and special-modes fields // and already tested alongside os:chmod, so we don't repeat those. ~> os:mkdir dir ~> print 123456 > file ~> put (os:stat file)[name type size] ▶ file ▶ regular ▶ (num 6) // size exists for directories on all platforms, but the value is // platform-depedent. ~> put (os:stat dir)[name type] ▶ dir ▶ dir ## can't stat non-existent file (Unix) ## //only-on unix ~> os:stat non-existent Exception: lstat non-existent: no such file or directory [tty]:1:1-20: os:stat non-existent ## can't stat non-existent file (Windows) ## //only-on windows // Windows has a different error message. ~> os:stat non-existent Exception: CreateFile non-existent: The system cannot find the file specified. [tty]:1:1-20: os:stat non-existent ## symlink ## //only-if-can-create-symlink ~> echo > regular os:symlink regular symlink ~> put (os:stat symlink)[type] ▶ symlink ~> put (os:stat &follow-symlink symlink)[type] ▶ regular ## fifo ## //mkfifo-or-skip fifo ~> put (os:stat fifo)[type] ▶ named-pipe ## sock on Unix ## //mksock-or-skip sock //only-on unix ~> put (os:stat sock)[type] ▶ socket ## sock on Windows ## //mksock-or-skip sock //only-on windows // Windows does support Unix sockets now, but the type is not reflected in what // we get from os.Stat. ~> put (os:stat sock)[type] ▶ irregular ## device ## //only-on unix ~> put (os:stat /dev/null)[type] ▶ char-device ## sys on Unix ## //only-on unix ~> echo > file var sys = (os:stat file)[sys] put $sys[nlink] and (each {|f| has-key $sys $f} [dev ino uid gid rdev blksize blocks]) ▶ (num 1) ▶ $true ## sys on Windows ## //only-on windows //create-windows-special-files-or-skip ~> has-value (os:stat directory)[sys][file-attributes] directory ▶ $true ~> has-value (os:stat readonly)[sys][file-attributes] readonly ▶ $true ~> has-value (os:stat hidden)[sys][file-attributes] hidden ▶ $true ////////////////////////////////////////// # os:exists, os:is-dir and os:is-regular # ////////////////////////////////////////// ~> os:mkdir d ~> echo > d/f ~> os:exists $pwd ▶ $true ~> os:is-dir $pwd ▶ $true ~> os:is-regular $pwd ▶ $false ~> os:exists d ▶ $true ~> os:is-dir d ▶ $true ~> os:is-regular d ▶ $false ~> os:exists d/f ▶ $true ~> os:is-dir d/f ▶ $false ~> os:is-regular d/f ▶ $true ~> os:exists bad ▶ $false ~> os:is-dir bad ▶ $false ~> os:is-regular bad ▶ $false ///////////////////////// # symbolic link-related # ///////////////////////// //only-if-can-create-symlink // Set up symlinks to test. Each file is named as "s-" plus path relative to the // test directory root, with / changed to -. ~> os:mkdir d echo > d/f os:symlink f d/s-f # target is in same directory os:symlink d s-d # target is directory os:symlink d/f s-d-f # target is in subdirectory os:symlink bad s-bad # target doesn't exist // These tests can run on Windows, where the output of os:eval-symlinks will use // \ as the path separator, so we can't rely on the exact output. ~> use path // Not symlink ~> eq (os:eval-symlinks d/f) (path:join d f) ▶ $true // Leaf is symlink ~> eq (os:eval-symlinks d/s-f) (path:join d f) ▶ $true // Non-leaf is symlink ~> eq (os:eval-symlinks s-d/f) (path:join d f) ▶ $true ~> os:exists s-d ▶ $true ~> os:exists s-d &follow-symlink ▶ $true ~> os:exists s-d-f ▶ $true ~> os:exists s-d-f &follow-symlink ▶ $true ~> os:exists s-bad ▶ $true ~> os:exists s-bad &follow-symlink ▶ $false ~> os:exists bad ▶ $false ~> os:exists bad &follow-symlink ▶ $false ~> os:is-dir s-d ▶ $false ~> os:is-dir s-d &follow-symlink ▶ $true ~> os:is-dir s-d-f ▶ $false ~> os:is-dir s-d-f &follow-symlink ▶ $false ~> os:is-dir s-bad ▶ $false ~> os:is-dir s-bad &follow-symlink ▶ $false ~> os:is-dir bad ▶ $false ~> os:is-dir bad &follow-symlink ▶ $false ~> os:is-regular s-d ▶ $false ~> os:is-regular s-d &follow-symlink ▶ $false ~> os:is-regular s-d-f ▶ $false ~> os:is-regular s-d-f &follow-symlink ▶ $true ~> os:is-regular s-bad ▶ $false ~> os:is-regular s-bad &follow-symlink ▶ $false ~> os:is-regular bad ▶ $false ~> os:is-regular bad &follow-symlink ▶ $false ## os:eval-symlinks given non-existent file (Unix) ## //only-on unix ~> os:symlink bad s-bad ~> os:eval-symlinks s-bad Exception: lstat bad: no such file or directory [tty]:1:1-22: os:eval-symlinks s-bad ## os:eval-symlinks given non-existent file (Windows) ## // Windows has a different error message. //only-on windows ~> os:symlink bad s-bad ~> os:eval-symlinks s-bad Exception: CreateFile bad: The system cannot find the file specified. [tty]:1:1-22: os:eval-symlinks s-bad /////////////// # os:temp-dir # /////////////// //each:eval use re // default name template is elvish-* ~> var x = (os:temp-dir) os:remove $x re:match '[/\\]elvish-.*$' $x ▶ $true // explicit name template ~> var x = (os:temp-dir 'x-*.y') os:remove $x re:match '[/\\]x-.*\.y$' $x ▶ $true ## create in pwd ## //in-temp-dir ~> var x = (os:temp-dir &dir=.) os:remove $x re:match '^(\.[/\\])?elvish-.*$' $x ▶ $true ~> var x = (os:temp-dir &dir=. 'x-*.y') os:remove $x re:match '^(\.[/\\])?x-.*\.y$' $x ▶ $true ## arity check ## ~> os:temp-dir a b Exception: arity mismatch: arguments must be 0 to 1 values, but is 2 values [tty]:1:1-15: os:temp-dir a b //////////////// # os:temp-file # //////////////// //each:eval use re //each:eval use file ~> var f = (os:temp-file) re:match '[/\\]elvish-.*$' $f[name] file:close $f os:remove $f[name] ▶ $true ~> var f = (os:temp-file 'x-*.y') re:match '[/\\]x-.*\.y$' $f[name] file:close $f os:remove $f[name] ▶ $true ## create in pwd ## //in-temp-dir ~> var f = (os:temp-file &dir=.) re:match '^(\.[/\\])?elvish-.*$' $f[name] file:close $f os:remove $f[name] ▶ $true ~> var f = (os:temp-file &dir=. 'x-*.y') re:match '^(\.[/\\])?x-.*\.y$' $f[name] file:close $f os:remove $f[name] ▶ $true ## arity check ## ~> os:temp-file a b Exception: arity mismatch: arguments must be 0 to 1 values, but is 2 values [tty]:1:1-16: os:temp-file a b elvish-0.21.0/pkg/mods/os/os_test.go000066400000000000000000000020611465720375400172530ustar00rootroot00000000000000package os_test import ( "embed" "net" "os" "strconv" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "umask", func(t *testing.T, arg string) { testutil.Umask(t, must.OK1(strconv.Atoi(arg))) }, "mkfifo-or-skip", mkFifoOrSkip, "mksock-or-skip", func(t *testing.T, s string) { listener, err := net.Listen("unix", "./sock") if err != nil { t.Skipf("can't listen to UNIX socket: %v", err) } t.Cleanup(func() { listener.Close() }) }, "only-if-can-create-symlink", func(t *testing.T) { testutil.ApplyDir(testutil.Dir{"test-file": ""}) err := os.Symlink("test-file", "test-symlink") if err != nil { // On Windows we may or may not be able to create a symlink. t.Skipf("symlink: %v", err) } must.OK(os.Remove("test-file")) must.OK(os.Remove("test-symlink")) }, "create-windows-special-files-or-skip", createWindowsSpecialFileOrSkip, ) } elvish-0.21.0/pkg/mods/os/os_unix.go000066400000000000000000000000671465720375400172630ustar00rootroot00000000000000//go:build unix package os const DevTTY = "/dev/tty" elvish-0.21.0/pkg/mods/os/os_unix_test.go000066400000000000000000000003771465720375400203260ustar00rootroot00000000000000//go:build unix package os_test import ( "testing" "golang.org/x/sys/unix" "src.elv.sh/pkg/must" ) func mkFifoOrSkip(name string) { must.OK(unix.Mkfifo(name, 0o600)) } func createWindowsSpecialFileOrSkip(t *testing.T) { t.Skip("not on Windows") } elvish-0.21.0/pkg/mods/os/os_windows.go000066400000000000000000000000411465720375400177620ustar00rootroot00000000000000package os const DevTTY = "CON" elvish-0.21.0/pkg/mods/os/os_windows_test.go000066400000000000000000000012001465720375400210170ustar00rootroot00000000000000package os_test import ( "testing" "golang.org/x/sys/windows" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) func mkFifoOrSkip(t *testing.T, _ string) { t.Skip("can't make FIFO on Windows") } func createWindowsSpecialFileOrSkip(t *testing.T) { testutil.ApplyDir(testutil.Dir{ "directory": testutil.Dir{}, "readonly": "", "hidden": "", }) mustSetFileAttributes("readonly", windows.FILE_ATTRIBUTE_READONLY) mustSetFileAttributes("hidden", windows.FILE_ATTRIBUTE_HIDDEN) } func mustSetFileAttributes(name string, attr uint32) { must.OK(windows.SetFileAttributes(must.OK1(windows.UTF16PtrFromString(name)), attr)) } elvish-0.21.0/pkg/mods/os/special_modes.go000066400000000000000000000027771465720375400204200ustar00rootroot00000000000000package os import ( "io/fs" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Conversion between an Elvish list representation of special modes and Go's // bit flag representation. These are used from different places, but since they // are symmetrical, keeping them in the same file makes it easier to ensure they // are consistent. // // A special mode is one of the bits in [fs.FileMode] that is not part of // [fs.ModeType] or [fs.ModePerm]. We omit [fs.ModeAppend], [fs.ModeExclusive] // and [fs.ModeTemporary] since they are only used on Plan 9, which Elvish // doesn't support (yet) so we can't test them. // // TODO: Use a set as the Elvish representation when Elvish has lists. func specialModesFromIterable(v any) (fs.FileMode, error) { var mode fs.FileMode var errElem error errIterate := vals.Iterate(v, func(elem any) bool { switch elem { case "setuid": mode |= fs.ModeSetuid case "setgid": mode |= fs.ModeSetgid case "sticky": mode |= fs.ModeSticky default: errElem = errs.BadValue{What: "special mode", Valid: "setuid, setgid or sticky", Actual: vals.ToString(elem)} return false } return true }) if errIterate != nil { return 0, errIterate } if errElem != nil { return 0, errElem } return mode, nil } func specialModesToList(mode fs.FileMode) vals.List { l := vals.EmptyList if mode&fs.ModeSetuid != 0 { l = l.Conj("setuid") } if mode&fs.ModeSetgid != 0 { l = l.Conj("setgid") } if mode&fs.ModeSticky != 0 { l = l.Conj("sticky") } return l } elvish-0.21.0/pkg/mods/os/stat.go000066400000000000000000000020141465720375400165440ustar00rootroot00000000000000package os import ( "fmt" "io/fs" "src.elv.sh/pkg/eval/vals" ) var typeNames = map[fs.FileMode]string{ 0: "regular", fs.ModeDir: "dir", fs.ModeSymlink: "symlink", fs.ModeNamedPipe: "named-pipe", fs.ModeSocket: "socket", fs.ModeDevice: "device", fs.ModeDevice | fs.ModeCharDevice: "char-device", fs.ModeIrregular: "irregular", } // Implementation of the stat function itself is in os.go. func statMap(fi fs.FileInfo) vals.Map { mode := fi.Mode() typeName, ok := typeNames[mode.Type()] if !ok { // This shouldn't happen, but if there is a bug this gives us a bit of // information. typeName = fmt.Sprintf("unknown %d", mode.Type()) } return vals.MakeMap( "name", fi.Name(), "size", vals.Int64ToNum(fi.Size()), "type", typeName, "perm", int(mode&fs.ModePerm), "special-modes", specialModesToList(mode), "sys", statSysMap(fi.Sys())) // TODO: ModTime } elvish-0.21.0/pkg/mods/os/stat_bsd.go000066400000000000000000000004151465720375400173770ustar00rootroot00000000000000//go:build darwin || freebsd || netbsd || openbsd package os import "syscall" func init() { extraStatFields["gen"] = func(st *syscall.Stat_t) uint64 { return uint64(st.Gen) } extraStatFields["flags"] = func(st *syscall.Stat_t) uint64 { return uint64(st.Flags) } } elvish-0.21.0/pkg/mods/os/stat_unix.go000066400000000000000000000015041465720375400176120ustar00rootroot00000000000000//go:build unix package os import ( "syscall" "src.elv.sh/pkg/eval/vals" ) var extraStatFields = map[string]func(*syscall.Stat_t) uint64{} func statSysMap(sys any) vals.Map { st := sys.(*syscall.Stat_t) m := vals.MakeMap( "dev", stNum(st.Dev), "ino", stNum(st.Ino), "nlink", stNum(st.Nlink), "uid", stNum(st.Uid), "gid", stNum(st.Gid), "rdev", stNum(st.Rdev), // TODO: atim, mtim, ctim "blksize", stNum(st.Blksize), "blocks", stNum(st.Blocks), ) for name, f := range extraStatFields { m = m.Assoc(name, stNum(f(st))) } return m } // Converts a stat_t field to Num. All of the these fields are non-negative even // if they are signed, so we convert them to uint64 first. func stNum[T interface { int16 | uint16 | int32 | uint32 | int64 | uint64 }](x T) vals.Num { return vals.Uint64ToNum(uint64(x)) } elvish-0.21.0/pkg/mods/os/stat_windows.go000066400000000000000000000025051465720375400203230ustar00rootroot00000000000000package os import ( "syscall" "src.elv.sh/pkg/eval/vals" ) // Taken from // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants. // The syscall package only has a subset of these. // // Some of these attributes are redundant with fields in the outer stat map, but // we keep all of them for consistency. var fileAttributeNames = [...]struct { bit uint32 name string }{ {0x1, "readonly"}, {0x2, "hidden"}, {0x4, "system"}, {0x10, "directory"}, {0x20, "archive"}, {0x40, "device"}, {0x80, "normal"}, {0x100, "temporary"}, {0x200, "sparse-file"}, {0x400, "reparse-point"}, {0x800, "compressed"}, {0x1000, "offline"}, {0x2000, "not-content-indexed"}, {0x4000, "encrypted"}, {0x8000, "integrity-system"}, {0x10000, "virtual"}, {0x20000, "no-scrub-data"}, {0x40000, "ea"}, {0x80000, "pinned"}, {0x100000, "unpinned"}, {0x400000, "recall-on-data-access"}, } func statSysMap(sys any) vals.Map { attrData := sys.(*syscall.Win32FileAttributeData) // TODO: Make this a set when Elvish has a set type. fileAttributes := vals.EmptyList for _, attr := range fileAttributeNames { if attrData.FileAttributes&attr.bit != 0 { fileAttributes = fileAttributes.Conj(attr.name) } } return vals.MakeMap( "file-attributes", fileAttributes, // TODO: CreationTime, LastAccessTime, LastWriteTime ) } elvish-0.21.0/pkg/mods/path/000077500000000000000000000000001465720375400155605ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/path/path.d.elv000066400000000000000000000071411465720375400174510ustar00rootroot00000000000000# Compatibility alias for [`$os:dev-null`](). This variable will be formally # deprecated and removed in future. var dev-null # Compatibility alias for [`$os:dev-tty`](). This variable will be formally # deprecated and removed in future. var dev-tty # OS-specific path list separator. Colon (`:`) on Unix and semicolon (`;`) on # Windows. This variable is read-only. var list-separator # OS-specific path separator. Forward slash (`/`) on Unix and backslash (`\`) # on Windows. This variable is read-only. var separator # Outputs `$path` converted to an absolute path. # # ```elvish-transcript # ~> cd ~ # ~> path:abs bin # ▶ /home/user/bin # ``` fn abs {|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 # ``` fn base {|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 # ``` fn clean {|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 # ``` fn dir {|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 # ``` fn ext {|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 # ``` fn is-abs {|path| } # Compatibility alias for [`os:eval-symlinks`](). This function will be formally # deprecated and removed in future. fn eval-symlinks {|path| } # Joins any number of path elements into a single path, separating them with an # [OS specific separator](#$path:separator). Empty elements are ignored. The # result is [cleaned](#path:clean). However, if the argument list is empty or # all its elements are empty, Join returns an empty string. On Windows, the # result will only be a UNC path if the first non-empty element is a UNC path. # # ```elvish-transcript # ~> path:join home user bin # ▶ home/user/bin # ~> path:join $path:separator home user bin # ▶ /home/user/bin # ``` fn join {|@path-component| } # Compatibility alias for [`os:is-dir`](). This function will be formally # deprecated and removed in future. fn is-dir {|&follow-symlink=$false path| } # Compatibility alias for [`os:is-regular`](). This function will be formally # deprecated and removed in future. fn is-regular {|&follow-symlink=$false path| } # Compatibility alias for [`os:temp-dir`](). This function will be formally # deprecated and removed in future. fn temp-dir {|&dir='' pattern?| } # Compatibility alias for [`os:temp-file`](). This function will be formally # deprecated and removed in future. fn temp-file {|&dir='' pattern?| } elvish-0.21.0/pkg/mods/path/path.go000066400000000000000000000021011465720375400170350ustar00rootroot00000000000000// 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/vars" osmod "src.elv.sh/pkg/mods/os" ) // Ns is the namespace for the path: module. var Ns = eval.BuildNsNamed("path"). AddVars(map[string]vars.Var{ "dev-null": vars.NewReadOnly(os.DevNull), "dev-tty": vars.NewReadOnly(osmod.DevTTY), "list-separator": vars.NewReadOnly(string(filepath.ListSeparator)), "separator": vars.NewReadOnly(string(filepath.Separator)), }). AddGoFns(map[string]any{ "abs": filepath.Abs, "base": filepath.Base, "clean": filepath.Clean, "dir": filepath.Dir, "ext": filepath.Ext, "is-abs": filepath.IsAbs, "join": filepath.Join, // Compatibility aliases; these have moved to os: but are kept here // until we can properly emit deprecation messages. "eval-symlinks": filepath.EvalSymlinks, "is-dir": osmod.IsDir, "is-regular": osmod.IsRegular, "temp-dir": osmod.TempDir, "temp-file": osmod.TempFile, }).Ns() elvish-0.21.0/pkg/mods/path/path_test.elvts000066400000000000000000000032021465720375400206270ustar00rootroot00000000000000//each:eval use path // All the functions in path: are either simple wrappers of Go functions or // compatibility aliases of their os: counterparts. // // As a result, the tests are just simple "smoke tests" to ensure that they // exist and map to the correct function. ///////////// # functions # ///////////// ~> use str var abs = (path:abs a/b/c.png) path:is-abs $abs str:has-suffix $abs (path:join a b c.png) ▶ $true ▶ $true ~> path:base a/b/d.png ▶ d.png ~> path:clean ././x ▶ x ~> path:ext a/b/e.png ▶ .png ~> path:ext a/b/s ▶ '' ~> path:is-abs a/b/s ▶ $false ///////////////// # Unix-specific # ///////////////// //only-on unix ~> put $path:dev-null ▶ /dev/null ~> put $path:dev-tty ▶ /dev/tty ~> put $path:list-separator ▶ : ~> put $path:separator ▶ / ~> path:join a b c ▶ a/b/c ~> path:clean a/b/.././c ▶ a/c ~> path:dir a/b/d.png ▶ a/b //////////////////// # Windows-specific # //////////////////// //only-on windows ~> put $path:dev-null ▶ NUL ~> put $path:dev-tty ▶ CON ~> put $path:list-separator ▶ ';' ~> put $path:separator ▶ \ ~> path:join a b c ▶ a\b\c ~> path:clean a/b/.././c ▶ a\c ~> path:dir a/b/d.png ▶ a\b ///////////////////////// # compatibility aliases # ///////////////////////// //in-temp-dir-with-d-f ~> use file use re use os ~> path:eval-symlinks d ▶ d ~> path:is-dir d ▶ $true ~> path:is-regular d/f ▶ $true ~> var x = (path:temp-dir) re:match '^.*'(re:quote $path:separator)'elvish-.*$' $x os:remove $x ▶ $true ~> var f = (path:temp-file) re:match '^.*'(re:quote $path:separator)'elvish-.*$' $f[name] file:close $f os:remove $f[name] ▶ $true elvish-0.21.0/pkg/mods/path/path_test.go000066400000000000000000000006261465720375400201060ustar00rootroot00000000000000package path_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "in-temp-dir-with-d-f", func(t *testing.T) { testutil.InTempDir(t) testutil.ApplyDir(testutil.Dir{ "d": testutil.Dir{ "f": "", }, }) }, ) } elvish-0.21.0/pkg/mods/platform/000077500000000000000000000000001465720375400164505ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/platform/platform.d.elv000066400000000000000000000022151465720375400212260ustar00rootroot00000000000000# 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. var arch # 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. var os # 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. var is-unix # Whether or not the platform is Microsoft Windows. # This is read-only. var is-windows # 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 # ``` fn hostname {|&strip-domain=$false| } elvish-0.21.0/pkg/mods/platform/platform.go000066400000000000000000000017101465720375400206220ustar00rootroot00000000000000// 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" ) 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]any{ "hostname": hostname, }).Ns() elvish-0.21.0/pkg/mods/platform/platform_test.elvts000066400000000000000000000023061465720375400224130ustar00rootroot00000000000000//each:eval use platform ////////////////// # $platform:arch # ////////////////// ## arm64 ## //only-on arm64 ~> put $platform:arch ▶ arm64 ## amd64 ## //only-on amd64 ~> put $platform:arch ▶ amd64 //////////////////////////// # $platform:os and friends # //////////////////////////// ## linux ## //only-on linux ~> put $platform:os ▶ linux ## darwin ## //only-on darwin ~> put $platform:os ▶ darwin ## freebsd ## //only-on freebsd ~> put $platform:os ▶ freebsd ## openbsd ## //only-on openbsd ~> put $platform:os ▶ openbsd ## netbsd ## //only-on netbsd ~> put $platform:os ▶ netbsd ## unix ## //only-on unix ~> put $platform:is-windows ▶ $false ~> put $platform:is-unix ▶ $true ## windows ## //only-on windows ~> put $platform:os ▶ windows ~> put $platform:is-windows ▶ $true ~> put $platform:is-unix ▶ $false ///////////////////// # platform:hostname # ///////////////////// ## good hostname ## //mock-hostname mach1.domain.tld ~> platform:hostname ▶ mach1.domain.tld ~> platform:hostname &strip-domain ▶ mach1 ## bad hostname ## //mock-hostname-error hostname cannot be determined ~> platform:hostname Exception: hostname cannot be determined [tty]:1:1-17: platform:hostname elvish-0.21.0/pkg/mods/platform/platform_test.go000066400000000000000000000011451465720375400216630ustar00rootroot00000000000000package platform_test import ( "embed" "errors" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods/platform" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "mock-hostname", func(t *testing.T, hostname string) { testutil.Set(t, platform.OSHostname, func() (string, error) { return hostname, nil }) }, "mock-hostname-error", func(t *testing.T, msg string) { err := errors.New(msg) testutil.Set(t, platform.OSHostname, func() (string, error) { return "", err }) }, ) } elvish-0.21.0/pkg/mods/platform/testexport_test.go000066400000000000000000000000571465720375400222610ustar00rootroot00000000000000package platform var OSHostname = &osHostname elvish-0.21.0/pkg/mods/platform/unix.go000066400000000000000000000001221465720375400177550ustar00rootroot00000000000000//go:build unix package platform const ( isUnix = true isWindows = false ) elvish-0.21.0/pkg/mods/platform/windows.go000066400000000000000000000001251465720375400204670ustar00rootroot00000000000000//go:build windows package platform const ( isUnix = false isWindows = true ) elvish-0.21.0/pkg/mods/re/000077500000000000000000000000001465720375400152325ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/re/match.go000066400000000000000000000004321465720375400166540ustar00rootroot00000000000000package 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.21.0/pkg/mods/re/re.d.elv000066400000000000000000000116471465720375400166030ustar00rootroot00000000000000#//each:eval use re # Quote `$string` for use in a pattern. Examples: # # ```elvish-transcript # ~> re:quote a.txt # ▶ a\.txt # ~> re:quote '(*)' # ▶ '\(\*\)' # ``` fn quote {|string| } # 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 # ``` fn match {|&posix=$false 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 # ▶ [&end=(num 1) &groups=[[&end=(num 1) &start=(num 0) &text=a]] &start=(num 0) &text=a] # ▶ [&end=(num 2) &groups=[[&end=(num 2) &start=(num 1) &text=b]] &start=(num 1) &text=b] # ~> re:find '[A-Z]([0-9])' 'A1 B2' # ▶ [&end=(num 2) &groups=[[&end=(num 2) &start=(num 0) &text=A1] [&end=(num 2) &start=(num 1) &text=1]] &start=(num 0) &text=A1] # ▶ [&end=(num 5) &groups=[[&end=(num 5) &start=(num 3) &text=B2] [&end=(num 5) &start=(num 4) &text=2]] &start=(num 3) &text=B2] # ``` fn find {|&posix=$false &longest=$false &max=-1 pattern 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' # ``` fn replace {|&posix=$false &longest=$false &literal=$false pattern repl 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 # ``` fn split {|&posix=$false &longest=$false &max=-1 pattern source| } # For each [value input](builtin.html#value-inputs), calls `$f` with the input # followed by all its fields. # # The `&sep` option is a regular expression for the field separator. For the # `&sep-posix` and `&sep-longest` options, see the # [introduction](#introduction); the `sep-` prefix is added for clarity. # # Calling [`break`]() in `$f` exits both `$f` and `re:awk`, and can be used to # stop processing inputs early. Calling [`continue`]() exits `$f` but not # `re:awk`, and can be used to stop `$f` early but continue processing inputs. # # This command allows you to write code resembling # [AWK](https://en.wikipedia.org/wiki/AWK) scripts, using an anonymous function # instead of a string containing AWK code. A simple example: # # ```elvish-transcript # ~> echo " lorem ipsum\n1 2" | awk '{ print $1 }' # lorem # 1 # ~> echo " lorem ipsum\n1 2" | re:awk {|line a b| put $a } # ▶ lorem # ▶ 1 # ``` # # **Note**: Since Elvish allows variable names consisting solely of digits, you # can do something like this to emulate AWK even more closely: # # ```elvish-transcript # ~> echo " lorem ipsum\n1 2" | re:awk {|0 1 2| put $1 } # ▶ lorem # ▶ 1 # ``` # # If the number of fields differ between lines, use a rest argument: # # ```elvish-transcript # ~> echo "a b\nc d e" | re:awk {|@a| echo (- (count $a) 1)' fields' } # 2 fields # 3 fields # ``` # # This command is roughly equivalent to the following Elvish function: # # ```elvish # fn my-awk {|&sep='[ \t]+' &sep-posix=$false &sep-longest=$false f @rest| # each {|line| # var @fields = (re:split $sep &posix=$sep-posix &longest=$sep-longest (str:trim $line " \t")) # $f $line $@fields # } $@rest # } # ``` fn awk {|&sep='[ \t]+' &sep-posix=$false &sep-longest=$false f inputs?| } elvish-0.21.0/pkg/mods/re/re.go000066400000000000000000000117231465720375400161730ustar00rootroot00000000000000// Package re implements a regular expression module. package re import ( "errors" "regexp" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) // Ns is the namespace for the re: module. var Ns = eval.BuildNsNamed("re"). AddGoFns(map[string]any{ "quote": regexp.QuoteMeta, "match": match, "find": find, "replace": replace, "split": split, "awk": awk, }).Ns() 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 } // 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 := vals.EmptyList 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.Conj(submatchStruct{text, start, end}) } err := out.Put(matchStruct{source[start:end], start, end, groups}) if err != nil { return err } } return nil } type replaceOpts struct { Posix bool Longest bool Literal bool } func (*replaceOpts) SetDefaultOptions() {} func replace(fm *eval.Frame, opts replaceOpts, argPattern string, argRepl any, 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 "", &errs.BadValue{What: "literal replacement", Valid: "string", Actual: 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, []any{s}, eval.NoOpts) }) if err != nil { errReplace = err return "" } if len(values) != 1 { errReplace = &errs.ArityMismatch{What: "replacement function output", ValidLow: 1, ValidHigh: 1, Actual: len(values)} return "" } output, ok := values[0].(string) if !ok { errReplace = &errs.BadValue{What: "replacement function output", Valid: "string", Actual: vals.Kind(values[0])} return "" } return output } return pattern.ReplaceAllStringFunc(source, replFunc), errReplace default: return "", &errs.BadValue{What: "replacement", Valid: "string or function", Actual: vals.Kind(argRepl)} } } 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 } // ErrInputOfAwkMustBeString is thrown when re:awk gets a non-string input. var ErrInputOfAwkMustBeString = errors.New("input of re:awk must be string") type awkOpt struct { Sep string SepPosix bool SepLongest bool } func (o *awkOpt) SetDefaultOptions() { o.Sep = "[ \t]+" } func awk(fm *eval.Frame, opts awkOpt, f eval.Callable, inputs eval.Inputs) error { wordSep, err := makePattern(opts.Sep, opts.SepPosix, opts.SepLongest) if err != nil { return err } broken := false inputs(func(v any) { if broken { return } line, ok := v.(string) if !ok { broken = true err = ErrInputOfAwkMustBeString return } args := []any{line} for _, field := range wordSep.Split(strings.Trim(line, " \t"), -1) { args = append(args, field) } newFm := fm.Fork() // TODO: Close port 0 of newFm. ex := f.Call(newFm, args, eval.NoOpts) newFm.Close() if ex != nil { switch eval.Reason(ex) { case nil, eval.Continue: // nop case eval.Break: broken = true default: broken = true err = ex } } }) return err } 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.21.0/pkg/mods/re/re_test.elvts000066400000000000000000000123051465720375400177570ustar00rootroot00000000000000//each:eval use re //////////// # re:match # //////////// ~> re:match . xyz ▶ $true ~> re:match . '' ▶ $false ~> re:match '[a-z]' A ▶ $false ## invalid pattern ## ~> re:match '(' x Exception: error parsing regexp: missing closing ): `(` [tty]:1:1-14: re:match '(' x /////////// # re:find # /////////// ~> re:find . ab ▶ [&end=(num 1) &groups=[[&end=(num 1) &start=(num 0) &text=a]] &start=(num 0) &text=a] ▶ [&end=(num 2) &groups=[[&end=(num 2) &start=(num 1) &text=b]] &start=(num 1) &text=b] ~> re:find '[A-Z]([0-9])' 'A1 B2' ▶ [&end=(num 2) &groups=[[&end=(num 2) &start=(num 0) &text=A1] [&end=(num 2) &start=(num 1) &text=1]] &start=(num 0) &text=A1] ▶ [&end=(num 5) &groups=[[&end=(num 5) &start=(num 3) &text=B2] [&end=(num 5) &start=(num 4) &text=2]] &start=(num 3) &text=B2] ## access to fields in the match StructMap ## ~> put (re:find . a)[text start end groups] ▶ a ▶ (num 0) ▶ (num 1) ▶ [[&end=(num 1) &start=(num 0) &text=a]] ## invalid pattern ## ~> re:find '(' x Exception: error parsing regexp: missing closing ): `(` [tty]:1:1-13: re:find '(' x ## without any flag, finds ax ## ~> put (re:find 'a(x|xy)' AaxyZ)[text] ▶ ax ## with &longest, finds axy ## ~> put (re:find &longest 'a(x|xy)' AaxyZ)[text] ▶ axy ## basic verification of &posix behavior. ## ~> put (re:find &posix 'a(x|xy)+' AaxyxxxyZ)[text] ▶ axyxxxy ## bubbles output error ## ~> re:find . ab >&- Exception: port does not support value output [tty]:1:1-16: re:find . ab >&- ////////////// # re:replace # ////////////// ~> re:replace '(ba|z)sh' '${1}SH' 'bash and zsh' ▶ 'baSH and zSH' ~> re:replace &literal '(ba|z)sh' '$sh' 'bash and zsh' ▶ '$sh and $sh' ~> re:replace '(ba|z)sh' {|x| put [&bash=BaSh &zsh=ZsH][$x] } 'bash and zsh' ▶ 'BaSh and ZsH' ## invalid pattern ## ~> re:replace '(' x bash Exception: error parsing regexp: missing closing ): `(` [tty]:1:1-21: re:replace '(' x bash ~> re:replace &posix '[[:argle:]]' x bash Exception: error parsing regexp: invalid character class range: `[:argle:]` [tty]:1:1-38: re:replace &posix '[[:argle:]]' x bash ## replacement function outputs more than one value ## ~> re:replace x {|x| put a b } xx Exception: arity mismatch: replacement function output must be 1 value, but is 2 values [tty]:1:1-30: re:replace x {|x| put a b } xx ## replacement function outputs non-string value ## ~> re:replace x {|x| put [] } xx Exception: bad value: replacement function output must be string, but is list [tty]:1:1-29: re:replace x {|x| put [] } xx ## replacement is not string or function ## ~> re:replace x [] xx Exception: bad value: replacement must be string or function, but is list [tty]:1:1-18: re:replace x [] xx ## replacement is function when &literal is set ## ~> re:replace &literal x {|_| put y } xx Exception: bad value: literal replacement must be string, but is fn [tty]:1:1-37: re:replace &literal x {|_| put y } xx //////////// # re:split # //////////// ~> 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 ## invalid pattern ## ~> re:split '(' x Exception: error parsing regexp: missing closing ): `(` [tty]:1:1-14: re:split '(' x ## bubbles output error ## ~> re:split . ab >&- Exception: port does not support value output [tty]:1:1-17: re:split . ab >&- //////////// # re:quote # //////////// ~> re:quote a.txt ▶ a\.txt ~> re:quote '(*)' ▶ '\(\*\)' ////////// # re:awk # ////////// ~> echo " ax by cz \n11\t22 33" | re:awk {|@a| put $a[-1] } ▶ cz ▶ 33 ## bad input type ## ~> num 42 | re:awk {|@a| fail "this should not run" } Exception: input of re:awk must be string [tty]:1:10-50: num 42 | re:awk {|@a| fail "this should not run" } ## propagating exception ## ~> to-lines [1 2 3 4] | re:awk {|@a| if (==s 3 $a[1]) { fail "stop re:awk" } put $a[1] } ▶ 1 ▶ 2 Exception: stop re:awk [tty]:3:9-26: fail "stop re:awk" [tty]:1:22-6:1: to-lines [1 2 3 4] | re:awk {|@a| if (==s 3 $a[1]) { fail "stop re:awk" } put $a[1] } ## break ## ~> to-lines [" a" "b\tc " "d" "e"] | re:awk {|@a| if (==s d $a[1]) { break } else { put $a[-1] } } ▶ a ▶ c ## continue ## ~> to-lines [" a" "b\tc " "d" "e"] | re:awk {|@a| if (==s d $a[1]) { continue } else { put $a[-1] } } ▶ a ▶ c ▶ e ## parsing docker image ls output with custom separator ## ~> to-lines [ 'REPOSITORY TAG IMAGE ID CREATED SIZE' ' 265c2d25a944 16 minutes ago 67.5 MB' ' 26408a88b236 16 minutes ago 389 MB' 'localhost/elvish_re:awk latest 0570db4e3eaa 32 hours ago 67.5 MB' 'localhost/elvish latest 59b1eec93ab7 33 hours ago 67.5 MB' 'docker.io/library/golang latest 015e6b7f599b 46 hours ago 838 MB' 'docker.io/library/golang 1.20-alpine 93db368a0a9e 3 days ago 266 MB' ] | re:awk &sep=" [ ]+" {|0 1 2 3 4 5| put $5 } ▶ SIZE ▶ '67.5 MB' ▶ '389 MB' ▶ '67.5 MB' ▶ '67.5 MB' ▶ '838 MB' ▶ '266 MB' elvish-0.21.0/pkg/mods/re/re_test.go000066400000000000000000000003321465720375400172240ustar00rootroot00000000000000package re_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts *.elv var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/readline-binding/000077500000000000000000000000001465720375400200175ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/readline-binding/readline-binding.elv000066400000000000000000000036001465720375400237210ustar00rootroot00000000000000set edit:global-binding[Ctrl-G] = $edit:close-mode~ { var 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 # Some functionalities bound to Ctrl-$key are occupied by readline binding, # use Alt-$key instead. $b Alt-n $edit:navigation:start~ $b Alt-l $edit:location:start~ $b Alt-a $edit:apply-autofix~ $b Ctrl-t $edit:transpose-rune~ $b Alt-t $edit:transpose-word~ } { var 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~ } { var 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~ } { var b = {|k f| set edit:history:binding[$k] = $f } $b Ctrl-N $edit:history:down-or-quit~ $b Ctrl-P $edit:history:up~ } { var 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~ } { var b = {|k f| set edit:histlist:binding[$k] = $f } $b Alt-d $edit:histlist:toggle-dedup~ } elvish-0.21.0/pkg/mods/readline-binding/readline-binding_test.elvts000066400000000000000000000001621465720375400253270ustar00rootroot00000000000000//prepare-deps // A smoke test to ensure that the readline-binding module has no errors. ~> use readline-binding elvish-0.21.0/pkg/mods/readline-binding/readlinebinding.go000066400000000000000000000002401465720375400234600ustar00rootroot00000000000000package readline_binding import _ "embed" // Code contains the source code of the readline-binding module. // //go:embed readline-binding.elv var Code string elvish-0.21.0/pkg/mods/readline-binding/readlinebinding_test.go000066400000000000000000000007761465720375400245350ustar00rootroot00000000000000package readline_binding_test import ( "embed" "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" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "prepare-deps", 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)) }) } elvish-0.21.0/pkg/mods/runtime/000077500000000000000000000000001465720375400163075ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/runtime/runtime.d.elv000066400000000000000000000022231465720375400207230ustar00rootroot00000000000000# A list containing # [module search directories](command.html#module-search-directories). # # This variable is read-only. var lib-dirs # Path to the [RC file](command.html#rc-file), ignoring any possible overrides # by command-line flags and available in non-interactive mode. # # If there was an error in determining the path of the RC file, this variable # is `$nil`. # # This variable is read-only. # # See also [`$runtime:effective-rc-path`](). var rc-path # Path to the [RC path](command.html#rc-file) that is actually used for this # Elvish session: # # - If Elvish is running non-interactively or invoked with the `-norc` flag, # this variable is `$nil`. # # - If Elvish is invoked with the `-rc` flag, this variable contains the # absolute path of the argument. # # - Otherwise (when Elvish is running interactively and invoked without # `-norc` or `-rc`), this variable has the same value as `$rc-path`. # # This variable is read-only. # # See also [`$runtime:rc-path`](). var effective-rc-path # The path to the Elvish binary. # # If there was an error in determining the path, this variable is `$nil`. # # This variable is read-only. var elvish-path elvish-0.21.0/pkg/mods/runtime/runtime.go000066400000000000000000000016101465720375400203170ustar00rootroot00000000000000// Package runtime implements the runtime module. package runtime import ( "os" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/eval/vars" ) var osExecutable = os.Executable // Ns returns the namespace for the runtime: module. // // All the public properties of the Evaler should be set before this function is // called. func Ns(ev *eval.Evaler) *eval.Ns { elvishPath, err := osExecutable() if err != nil { elvishPath = "" } return eval.BuildNsNamed("runtime"). AddVars(map[string]vars.Var{ "elvish-path": vars.NewReadOnly(nonEmptyOrNil(elvishPath)), "lib-dirs": vars.NewReadOnly(vals.MakeListSlice(ev.LibDirs)), "rc-path": vars.NewReadOnly(nonEmptyOrNil(ev.RcPath)), "effective-rc-path": vars.NewReadOnly(nonEmptyOrNil(ev.EffectiveRcPath)), }).Ns() } func nonEmptyOrNil(s string) any { if s == "" { return nil } return s } elvish-0.21.0/pkg/mods/runtime/runtime_test.elvts000066400000000000000000000007251465720375400221140ustar00rootroot00000000000000# runtime module with good paths # //use-runtime-good-paths ~> put $runtime:lib-dirs ▶ [/lib/1 /lib/2] ~> put $runtime:rc-path ▶ /path/to/rc.elv ~> put $runtime:effective-rc-path ▶ /path/to/effective/rc.elv ~> put $runtime:elvish-path ▶ /path/to/elvish # runtime module with bad paths # //use-runtime-bad-paths ~> put $runtime:lib-dirs ▶ [] ~> put $runtime:elvish-path ▶ $nil ~> put $runtime:rc-path ▶ $nil ~> put $runtime:effective-rc-path ▶ $nil elvish-0.21.0/pkg/mods/runtime/runtime_test.go000066400000000000000000000021461465720375400213630ustar00rootroot00000000000000package runtime_test import ( "embed" "errors" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/mods/runtime" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, // We can't rely on the default runtime module installed by evaltest // because the runtime modules reads Evaler fields during // initialization. "use-runtime-good-paths", func(t *testing.T, ev *eval.Evaler) { testutil.Set(t, runtime.OSExecutable, func() (string, error) { return "/path/to/elvish", nil }) ev.LibDirs = []string{"/lib/1", "/lib/2"} ev.RcPath = "/path/to/rc.elv" ev.EffectiveRcPath = "/path/to/effective/rc.elv" ev.ExtendGlobal(eval.BuildNs().AddNs("runtime", runtime.Ns(ev))) }, "use-runtime-bad-paths", func(t *testing.T, ev *eval.Evaler) { testutil.Set(t, runtime.OSExecutable, func() (string, error) { return "bad", errors.New("bad") }) // Leaving all the path fields in ev unset ev.ExtendGlobal(eval.BuildNs().AddNs("runtime", runtime.Ns(ev))) }, ) } elvish-0.21.0/pkg/mods/runtime/testexport_test.go000066400000000000000000000000621465720375400221140ustar00rootroot00000000000000package runtime var OSExecutable = &osExecutable elvish-0.21.0/pkg/mods/store/000077500000000000000000000000001465720375400157605ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/store/store.d.elv000066400000000000000000000025361465720375400200540ustar00rootroot00000000000000# Outputs the sequence number that will be used for the next entry of the # command history. fn next-cmd-seq { } # Adds an entry to the command history with the given content. Outputs its # sequence number. fn add-cmd {|text| } # 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. fn del-cmd {|seq| } # Outputs the content of the command history entry with the given sequence # number. fn cmd {|seq| } # 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`. fn cmds {|from upto| } # Adds a path to the directory history. This will also cause the scores of all # other directories to decrease. fn add-dir {|path| } # Deletes a path from the directory history. This has no impact on the scores # of other directories. fn del-dir {|path| } # Outputs all directory history entries, in decreasing order of score. # # Each entry is represented by a pseudo-map with fields `path` and `score`. fn dirs { } elvish-0.21.0/pkg/mods/store/store.go000066400000000000000000000011131465720375400174370ustar00rootroot00000000000000package store import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/store/storedefs" ) func Ns(s storedefs.Store) *eval.Ns { return eval.BuildNsNamed("store"). AddGoFns(map[string]any{ "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) }, }).Ns() } elvish-0.21.0/pkg/mods/store/store_test.elvts000066400000000000000000000015311465720375400212320ustar00rootroot00000000000000//each:use-store-brand-new # command store # // add ~> store:next-cmd-seq ▶ (num 1) ~> store:add-cmd foo ▶ (num 1) ~> store:add-cmd bar ▶ (num 2) ~> store:add-cmd baz ▶ (num 3) ~> store:next-cmd-seq ▶ (num 4) // query ~> store:cmd 1 ▶ foo ~> store:cmds 1 4 ▶ [&seq=(num 1) &text=foo] ▶ [&seq=(num 2) &text=bar] ▶ [&seq=(num 3) &text=baz] ~> store:cmds 2 3 ▶ [&seq=(num 2) &text=bar] ~> store:next-cmd 1 f ▶ [&seq=(num 1) &text=foo] ~> store:prev-cmd 3 b ▶ [&seq=(num 2) &text=bar] // delete ~> store:del-cmd 2 ~> store:cmds 1 4 ▶ [&seq=(num 1) &text=foo] ▶ [&seq=(num 3) &text=baz] # directory store # // add ~> store:add-dir /foo ~> store:add-dir /bar // query ~> store:dirs ▶ [&path=/bar &score=(num 10.0)] ▶ [&path=/foo &score=(num 9.86)] // delete ~> store:del-dir /foo ~> store:dirs ▶ [&path=/bar &score=(num 10.0)] elvish-0.21.0/pkg/mods/store/store_test.go000066400000000000000000000007631465720375400205100ustar00rootroot00000000000000package store import ( "embed" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/must" "src.elv.sh/pkg/store" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "use-store-brand-new", func(t *testing.T, ev *eval.Evaler) { testutil.InTempDir(t) s := must.OK1(store.NewStore("db")) ev.ExtendGlobal(eval.BuildNs().AddNs("store", Ns(s))) }, ) } elvish-0.21.0/pkg/mods/str/000077500000000000000000000000001465720375400154345ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/str/str.d.elv000066400000000000000000000200241465720375400171740ustar00rootroot00000000000000#//each:eval use str # 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 # ▶ (num 0) # ~> str:compare a b # ▶ (num -1) # ~> str:compare b a # ▶ (num 1) # ``` fn compare {|a b| } # Outputs whether `$str` contains `$substr` as a substring. # # ```elvish-transcript # ~> str:contains abcd x # ▶ $false # ~> str:contains abcd bc # ▶ $true # ``` fn contains {|str substr| } # Outputs whether `$str` contains any Unicode code points in `$chars`. # # ```elvish-transcript # ~> str:contains-any abcd x # ▶ $false # ~> str:contains-any abcd xby # ▶ $true # ``` fn contains-any {|str chars| } # 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 # ▶ (num 2) # ~> str:count abcdef '' # ▶ (num 7) # ``` fn count {|str substr| } # 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 # ``` fn equal-fold {|str1 str2| } # Splits `$str` around each instance of one or more consecutive white space # characters. # # ```elvish-transcript # ~> str:fields "lorem ipsum dolor" # ▶ lorem # ▶ ipsum # ▶ dolor # ~> str:fields " " # ``` # # See also [`str:split`](). fn fields {|str| } # Outputs a string consisting of the given Unicode codepoints. Example: # # ```elvish-transcript # ~> str:from-codepoints 0x61 # ▶ a # ~> str:from-codepoints 0x4f60 0x597d # ▶ 你好 # ``` # # See also [`str:to-codepoints`](). fn from-codepoints {|@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 # ▶ 你好 # ``` # # See also [`str:to-utf8-bytes`](). fn from-utf8-bytes {|@number| } # Outputs if `$str` begins with `$prefix`. # # ```elvish-transcript # ~> str:has-prefix abc ab # ▶ $true # ~> str:has-prefix abc bc # ▶ $false # ``` fn has-prefix {|str prefix| } # Outputs if `$str` ends with `$suffix`. # # ```elvish-transcript # ~> str:has-suffix abc ab # ▶ $false # ~> str:has-suffix abc bc # ▶ $true # ``` fn has-suffix {|str suffix| } # 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 # ▶ (num 2) # ~> str:index abcd xyz # ▶ (num -1) # ``` fn index {|str substr| } # 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" # ▶ (num 2) # ~> str:index-any l33t aeiouy # ▶ (num -1) # ``` fn index-any {|str chars| } # 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). # # See also [`str:split`](). fn join {|sep input-list?| } # 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 # ▶ (num 12) # ~> str:last-index "elven speak elvish" romulan # ▶ (num -1) # ``` fn last-index {|str substr| } #doc:added-in 0.21 # Outputs a string consisting of `$n` copies of `$s`. # # Examples: # # ```elvish-transcript # ~> str:repeat a 10 # ▶ aaaaaaaaaa # ``` fn repeat {|s n| } # 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. fn replace {|&max=-1 old repl source| } # 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). # # See also [`str:join`]() and [`str:fields`](). fn split {|&max=-1 sep string| } # 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' # ``` fn title {|str| } # 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. # # See also [`str:from-codepoints`](). fn to-codepoints {|string| } # Outputs `$str` with all Unicode letters mapped to their lower-case # equivalent. # # ```elvish-transcript # ~> str:to-lower 'ABC!123' # ▶ abc!123 # ``` fn to-lower {|str| } # 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. # # See also [`str:from-utf8-bytes`](). fn to-utf8-bytes {|string| } # 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 "хлеб" # ▶ ХЛЕБ # ``` fn to-title {|str| } # Outputs `$str` with all Unicode letters mapped to their upper-case # equivalent. # # ```elvish-transcript # ~> str:to-upper 'abc!123' # ▶ ABC!123 # ``` fn to-upper { } # Outputs `$str` with all leading and trailing Unicode code points contained # in `$cutset` removed. # # ```elvish-transcript # ~> str:trim "¡¡¡Hello, Elven!!!" "!¡" # ▶ 'Hello, Elven' # ``` fn trim {|str cutset| } # Outputs `$str` with all leading Unicode code points contained in `$cutset` # removed. To remove a prefix string use [`str:trim-prefix`](). # # ```elvish-transcript # ~> str:trim-left "¡¡¡Hello, Elven!!!" "!¡" # ▶ 'Hello, Elven!!!' # ``` fn trim-left {|str cutset| } # 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!!!' # ``` fn trim-prefix {|str prefix| } # Outputs `$str` with all trailing Unicode code points contained in `$cutset` # removed. To remove a suffix string use [`str:trim-suffix`](). # # ```elvish-transcript # ~> str:trim-right "¡¡¡Hello, Elven!!!" "!¡" # ▶ '¡¡¡Hello, Elven' # ``` fn trim-right {|str cutset| } # 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' # ``` fn trim-space {|str| } # 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!!!' # ``` fn trim-suffix {|str suffix| } elvish-0.21.0/pkg/mods/str/str.go000066400000000000000000000104041465720375400165720ustar00rootroot00000000000000// 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]any{ "compare": strings.Compare, "contains": strings.Contains, "contains-any": strings.ContainsAny, "count": strings.Count, "equal-fold": strings.EqualFold, // TODO: FieldsFunc "fields": strings.Fields, "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": repeat, "replace": replace, "split": split, // TODO: SplitAfter //lint:ignore SA1019 Elvish builtins need to be formally deprecated // before removal "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() 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) } 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 } func join(sep string, inputs eval.Inputs) (string, error) { var buf bytes.Buffer var errJoin error first := true inputs(func(v any) { 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 } func repeat(s string, n int) (string, error) { if n < 0 { return "", errs.BadValue{What: "n", Valid: "non-negative number", Actual: vals.ToString(n)} } if len(s)*n < 0 { return "", errs.BadValue{What: "n", Valid: "small enough not to overflow result", Actual: vals.ToString(n)} } return strings.Repeat(s, n), nil } 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) } 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 } 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 } 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 } elvish-0.21.0/pkg/mods/str/str_test.elvts000066400000000000000000000207571465720375400203750ustar00rootroot00000000000000//each:eval use str /////////////// # str:compare # /////////////// ~> str:compare abc abc ▶ (num 0) ~> str:compare abc def ▶ (num -1) ~> str:compare def abc ▶ (num 1) ~> str:compare abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-15: str:compare abc //////////////// # str:contains # //////////////// ~> str:contains abcd x ▶ $false ~> str:contains abcd bc ▶ $true ~> str:contains abcd cde ▶ $false ~> str:contains abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-16: str:contains abc //////////////////// # str:contains-any # //////////////////// ~> str:contains-any abcd x ▶ $false ~> str:contains-any abcd xcy ▶ $true ~> str:contains-any abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-20: str:contains-any abc ////////////////// # str:equal-fold # ////////////////// ~> str:equal-fold ABC abc ▶ $true ~> str:equal-fold abc ABC ▶ $true ~> str:equal-fold abc A ▶ $false ~> str:equal-fold abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-18: str:equal-fold abc ////////////// # str:fields # ////////////// ~> str:fields "abc ABC" ▶ abc ▶ ABC ~> str:fields "abc ABC" ▶ abc ▶ ABC ~> str:fields " " /////////////////////// # str:from-codepoints # /////////////////////// ~> str:from-codepoints 0x61 ▶ a ~> str:from-codepoints 0x4f60 0x597d ▶ 你好 ~> str:from-codepoints -0x1 Exception: out of range: codepoint must be from 0 to 1114111, but is -0x1 [tty]:1:1-24: str:from-codepoints -0x1 ~> str:from-codepoints 0x110000 Exception: out of range: codepoint must be from 0 to 1114111, but is 0x110000 [tty]:1:1-28: str:from-codepoints 0x110000 ~> str:from-codepoints 0xd800 Exception: bad value: argument to str:from-codepoints must be valid Unicode codepoint, but is 0xd800 [tty]:1:1-26: str:from-codepoints 0xd800 /////////////////////// # str:from-utf8-bytes # /////////////////////// ~> str:from-utf8-bytes 0x61 ▶ a ~> str:from-utf8-bytes 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd ▶ 你好 ~> str:from-utf8-bytes -1 Exception: out of range: byte must be from 0 to 255, but is -1 [tty]:1:1-22: str:from-utf8-bytes -1 ~> str:from-utf8-bytes 256 Exception: out of range: byte must be from 0 to 255, but is 256 [tty]:1:1-23: str:from-utf8-bytes 256 ~> str:from-utf8-bytes 0xff 0x3 0xaa Exception: bad value: arguments to str:from-utf8-bytes must be valid UTF-8 sequence, but is [255 3 170] [tty]:1:1-33: str:from-utf8-bytes 0xff 0x3 0xaa ////////////////// # str:has-prefix # ////////////////// ~> str:has-prefix abcd ab ▶ $true ~> str:has-prefix abcd cd ▶ $false ~> str:has-prefix abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-18: str:has-prefix abc ////////////////// # str:has-suffix # ////////////////// ~> str:has-suffix abcd ab ▶ $false ~> str:has-suffix abcd cd ▶ $true ~> str:has-suffix abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-18: str:has-suffix abc ///////////// # str:index # ///////////// ~> str:index abcd cd ▶ (num 2) ~> str:index abcd de ▶ (num -1) ~> str:index abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-13: str:index abc ///////////////// # str:index-any # ///////////////// ~> str:index-any "chicken" "aeiouy" ▶ (num 2) ~> str:index-any l33t aeiouy ▶ (num -1) ~> str:index-any abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-17: str:index-any abc //////////// # str:join # //////////// ~> str:join : [/usr /bin /tmp] ▶ /usr:/bin:/tmp ~> str:join : ['' a ''] ▶ :a: ~> str:join : [(num 1) 2] Exception: bad value: input to str:join must be string, but is number [tty]:1:1-22: str:join : [(num 1) 2] ////////////////// # str:last-index # ////////////////// ~> str:last-index "elven speak elvish" "elv" ▶ (num 12) ~> str:last-index "elven speak elvish" "romulan" ▶ (num -1) ~> str:last-index abc Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-18: str:last-index abc ////////////// # str:repeat # ////////////// ~> str:repeat a 10 ▶ aaaaaaaaaa ~> str:repeat a -1 Exception: bad value: n must be non-negative number, but is -1 [tty]:1:1-15: str:repeat a -1 ## overflow (32-bit) ## //only-on 32bit ~> use math ~> str:repeat ab (- (math:pow 2 31) 1) Exception: bad value: n must be small enough not to overflow result, but is 2147483647 [tty]:1:1-35: str:repeat ab (- (math:pow 2 31) 1) ## overflow (64-bit) ## //only-on 64bit ~> use math ~> str:repeat ab (- (math:pow 2 63) 1) Exception: bad value: n must be small enough not to overflow result, but is 9223372036854775807 [tty]:1:1-35: str:repeat ab (- (math:pow 2 63) 1) /////////////// # str:replace # /////////////// ~> str:replace : / ":usr:bin:tmp" ▶ /usr/bin/tmp ~> str:replace &max=2 : / :usr:bin:tmp ▶ /usr/bin:tmp ///////////// # str:split # ///////////// ~> str:split : /usr:/bin:/tmp ▶ /usr ▶ /bin ▶ /tmp ~> str:split : /usr:/bin:/tmp &max=2 ▶ /usr ▶ /bin:/tmp ## propagates output errors ## ~> str:split : a:b >&- Exception: port does not support value output [tty]:1:1-19: str:split : a:b >&- ///////////////////// # str:to-codepoints # ///////////////////// ~> str:to-codepoints a ▶ 0x61 ~> str:to-codepoints 你好 ▶ 0x4f60 ▶ 0x597d ~> str:to-codepoints 你好 | str:from-codepoints (all) ▶ 你好 ~> str:to-codepoints a >&- Exception: port does not support value output [tty]:1:1-23: str:to-codepoints a >&- ///////////////////// # str:to-utf8-bytes # ///////////////////// ~> str:to-utf8-bytes a ▶ 0x61 ~> str:to-utf8-bytes 你好 ▶ 0xe4 ▶ 0xbd ▶ 0xa0 ▶ 0xe5 ▶ 0xa5 ▶ 0xbd ~> str:to-utf8-bytes 你好 | str:from-utf8-bytes (all) ▶ 你好 ## propagates output errors ## ~> str:to-utf8-bytes a >&- Exception: port does not support value output [tty]:1:1-23: str:to-utf8-bytes a >&- ///////////// # str:title # ///////////// ~> str:title abc ▶ Abc ~> str:title "abc def" ▶ 'Abc Def' //////////////// # str:to-lower # //////////////// ~> str:to-lower abc ▶ abc ~> str:to-lower ABC ▶ abc ~> str:to-lower ABC def Exception: arity mismatch: arguments must be 1 value, but is 2 values [tty]:1:1-20: str:to-lower ABC def ~> str:to-lower abc def Exception: arity mismatch: arguments must be 1 value, but is 2 values [tty]:1:1-20: str:to-lower abc def //////////////// # str:to-title # //////////////// ~> str:to-title "her royal highness" ▶ 'HER ROYAL HIGHNESS' ~> str:to-title "хлеб" ▶ ХЛЕБ //////////////// # str:to-upper # //////////////// ~> str:to-upper abc ▶ ABC ~> str:to-upper ABC ▶ ABC ~> str:to-upper ABC def Exception: arity mismatch: arguments must be 1 value, but is 2 values [tty]:1:1-20: str:to-upper ABC def //////////// # str:trim # //////////// ~> str:trim "¡¡¡Hello, Elven!!!" "!¡" ▶ 'Hello, Elven' ~> str:trim def Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-12: str:trim def ///////////////// # str:trim-left # ///////////////// ~> str:trim-left "¡¡¡Hello, Elven!!!" "!¡" ▶ 'Hello, Elven!!!' ~> str:trim-left def Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-17: str:trim-left def /////////////////// # str:trim-prefix # /////////////////// ~> str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hello, " ▶ Elven!!! ~> str:trim-prefix "¡¡¡Hello, Elven!!!" "¡¡¡Hola, " ▶ '¡¡¡Hello, Elven!!!' ~> str:trim-prefix def Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-19: str:trim-prefix def ////////////////// # str:trim-right # ////////////////// ~> str:trim-right "¡¡¡Hello, Elven!!!" "!¡" ▶ '¡¡¡Hello, Elven' ~> str:trim-right def Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-18: str:trim-right def ////////////////// # str:trim-space # ////////////////// ~> str:trim-space " \t\n Hello, Elven \n\t\r\n" ▶ 'Hello, Elven' ~> str:trim-space " \t\n Hello Elven \n\t\r\n" ▶ 'Hello Elven' ~> str:trim-space " \t\n Hello Elven \n\t\r\n" argle Exception: arity mismatch: arguments must be 1 value, but is 2 values [tty]:1:1-50: str:trim-space " \t\n Hello Elven \n\t\r\n" argle /////////////////// # str:trim-suffix # /////////////////// ~> str:trim-suffix "¡¡¡Hello, Elven!!!" ", Elven!!!" ▶ ¡¡¡Hello ~> str:trim-suffix "¡¡¡Hello, Elven!!!" ", Klingons!!!" ▶ '¡¡¡Hello, Elven!!!' ~> str:trim-suffix "¡¡¡Hello, Elven!!!" Exception: arity mismatch: arguments must be 2 values, but is 1 value [tty]:1:1-39: str:trim-suffix "¡¡¡Hello, Elven!!!" elvish-0.21.0/pkg/mods/str/str_test.go000066400000000000000000000003331465720375400176310ustar00rootroot00000000000000package str_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts *.elv var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/mods/unix/000077500000000000000000000000001465720375400156075ustar00rootroot00000000000000elvish-0.21.0/pkg/mods/unix/non_unix.go000066400000000000000000000005301465720375400177710ustar00rootroot00000000000000//go:build !unix 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. var Ns = &eval.Ns{} elvish-0.21.0/pkg/mods/unix/rlim_t_int64.go000066400000000000000000000011461465720375400204520ustar00rootroot00000000000000//go:build freebsd package unix import ( "math" "math/big" "strconv" ) type rlimT = int64 var rlimTValid = "number between 0 and " + strconv.FormatInt(math.MaxInt64, 10) const maxInt = int64(^uint(0) >> 1) func convertRlimT(x int64) any { if x <= maxInt { return int(x) } return big.NewInt(int64(x)) } func parseRlimT(val any) (int64, bool) { switch val := val.(type) { case int: return int64(val), true case *big.Int: if val.IsInt64() { return val.Int64(), true } case string: num, err := strconv.ParseInt(val, 0, 64) if err == nil { return num, true } } return 0, false } elvish-0.21.0/pkg/mods/unix/rlim_t_uint64.go000066400000000000000000000014061465720375400206360ustar00rootroot00000000000000//go:build unix && !freebsd package unix import ( "math" "math/big" "strconv" ) type rlimT = uint64 var rlimTValid = "number between 0 and " + strconv.FormatUint(math.MaxUint64, 10) const maxInt = uint64(^uint(0) >> 1) func convertRlimT(x uint64) any { if x <= maxInt { return int(x) } if x <= math.MaxInt64 { return big.NewInt(int64(x)) } z := big.NewInt(int64(x / 2)) z.Lsh(z, 1) if x%2 == 1 { z.Bits()[0] |= 1 } return z } func parseRlimT(val any) (uint64, bool) { switch val := val.(type) { case int: if val >= 0 { return uint64(val), true } case *big.Int: if val.IsUint64() { return val.Uint64(), true } case string: num, err := strconv.ParseUint(val, 0, 64) if err == nil { return num, true } } return 0, false } elvish-0.21.0/pkg/mods/unix/rlimit.d.elv000066400000000000000000000056261465720375400200520ustar00rootroot00000000000000# A map describing resource limits of the current process. # # Each key is a string corresponds to a resource, and each value is a map with # keys `&cur` and `&max`, describing the soft and hard limits of that resource. # A missing `&cur` key means that there is no soft limit; a missing `&max` key # means that there is no hard limit. # # The following resources are supported, some only present on certain OSes: # # * `core`: size of a core file, in bytes. # # * `cpu`: CPU time, in seconds. # # * `data`: size of the data segment, in bytes # # * `fsize`: size of a file created by the process, in bytes. # # * `memlock`: size of locked memory, in bytes. # # * `nofile`: number of file descriptors. # # * `nproc`: number of processes for the user. # # * `rss`: resident set size, in bytes. # # * `stack`: size of the stack segment, in bytes. # # * `as`: size of the address space, in bytes. Available on Linux, FreeBSD and NetBSD. # # * `nthr`: number of threads for the user. NetNSD only. # # * `sbsize`: size of socket buffers, in bytes. NetBSD only. # # * `locks`: number of file locks. Linux only. # # * `msgqueue`: size of message queues, in bytes. Linux only. # # * `nice`: ceiling of 20 - nice value. Linux only. # # * `rtprio`: real time priority. Linux only. # # * `rttime`: real-time CPU time, in seconds. Linux only. # # * `sigpending`: number of signals queued. Linux only. # # For the exact semantics of each resource, see the man page of `getrlimit`: # [Linux](https://man7.org/linux/man-pages/man2/setrlimit.2.html), # [macOS](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrlimit.2.html), # [FreeBSD](https://www.freebsd.org/cgi/man.cgi?query=getrlimit), # [NetBSD](https://man.netbsd.org/getrlimit.2), # [OpenBSD](https://man.openbsd.org/getrlimit.2). A key `foo` in the Elvish API # corresponds to `RLIMIT_FOO` in the C API. # # Examples: # # ```elvish-transcript # ~> put $unix:rlimits # ▶ [&nofile=[&cur=(num 256)] &fsize=[&] &nproc=[&max=(num 2666) &cur=(num 2666)] &memlock=[&] &cpu=[&] &core=[&cur=(num 0)] &stack=[&max=(num 67092480) &cur=(num 8372224)] &rss=[&] &data=[&]] # ~> # mimic Bash's "ulimit -a" # ~> keys $unix:rlimits | order | each {|key| # var m = $unix:rlimits[$key] # fn get {|k| if (has-key $m $k) { put $m[$k] } else { put unlimited } } # printf "%-7v %-9v %-9v\n" $key (get cur) (get max) # } # core 0 unlimited # cpu unlimited unlimited # data unlimited unlimited # fsize unlimited unlimited # memlock unlimited unlimited # nofile 256 unlimited # nproc 2666 2666 # rss unlimited unlimited # stack 8372224 67092480 # ~> # Decrease the soft limit on file descriptors # ~> set unix:rlimits[nofile][cur] = 100 # ~> put $unix:rlimits[nofile] # ▶ [&cur=(num 100)] # ~> # Remove the soft limit on file descriptors # ~> del unix:rlimits[nofile][cur] # ~> put $unix:rlimits[nofile] # ▶ [&] # ``` var rlimits elvish-0.21.0/pkg/mods/unix/rlimit.go000066400000000000000000000101261465720375400174360ustar00rootroot00000000000000//go:build unix package unix import ( "fmt" "sync" "golang.org/x/sys/unix" "src.elv.sh/pkg/eval/errs" "src.elv.sh/pkg/eval/vals" ) type rlimitsVar struct{} var ( getRlimit = unix.Getrlimit setRlimit = unix.Setrlimit ) var ( rlimitMutex sync.Mutex rlimits map[int]*unix.Rlimit ) func (rlimitsVar) Get() any { rlimitMutex.Lock() defer rlimitMutex.Unlock() initRlimits() rlimitsMap := vals.EmptyMap for res, lim := range rlimits { limMap := vals.EmptyMap if lim.Cur != unix.RLIM_INFINITY { limMap = limMap.Assoc("cur", convertRlimT(lim.Cur)) } if lim.Max != unix.RLIM_INFINITY { limMap = limMap.Assoc("max", convertRlimT(lim.Max)) } rlimitsMap = rlimitsMap.Assoc(rlimitKeys[res], limMap) } return rlimitsMap } func (rlimitsVar) Set(v any) error { newRlimits, err := parseRlimitsMap(v) if err != nil { return err } rlimitMutex.Lock() defer rlimitMutex.Unlock() initRlimits() for res := range rlimits { if *rlimits[res] != *newRlimits[res] { err := setRlimit(res, newRlimits[res]) if err != nil { return fmt.Errorf("setrlimit %s: %w", rlimitKeys[res], err) } rlimits[res] = newRlimits[res] } } return nil } func initRlimits() { if rlimits != nil { return } rlimits = make(map[int]*unix.Rlimit) for res := range rlimitKeys { var lim unix.Rlimit err := getRlimit(res, &lim) if err == nil { rlimits[res] = &lim } else { // Since getrlimit should only ever return an error when the // resource is not supported, this should normally never happen. But // be defensive nonetheless. logger.Println("initialize rlimits", res, rlimitKeys[res], err) // Remove this key, so that rlimitKeys is always consistent with the // value of rlimits (and thus $unix:rlimits). delete(rlimitKeys, res) } } } func parseRlimitsMap(val any) (map[int]*unix.Rlimit, error) { if err := checkRlimitsMapKeys(val); err != nil { return nil, err } limits := make(map[int]*unix.Rlimit, len(rlimitKeys)) for res, key := range rlimitKeys { limitVal, err := vals.Index(val, key) if err != nil { return nil, err } limits[res], err = parseRlimitMap(limitVal) if err != nil { return nil, err } } return limits, nil } func checkRlimitsMapKeys(val any) error { wantedKeys := make(map[string]struct{}, len(rlimitKeys)) for _, key := range rlimitKeys { wantedKeys[key] = struct{}{} } var errKey error err := vals.IterateKeys(val, func(k any) bool { ks, ok := k.(string) if !ok { errKey = errs.BadValue{What: "key of $unix:rlimits", Valid: "string", Actual: vals.Kind(k)} return false } if _, valid := wantedKeys[ks]; !valid { errKey = errs.BadValue{What: "key of $unix:rlimits", Valid: "valid resource key", Actual: vals.ReprPlain(k)} return false } delete(wantedKeys, ks) return true }) if err != nil { return errs.BadValue{What: "$unix:rlimits", Valid: "map", Actual: vals.Kind(val)} } if errKey != nil { return errKey } if len(wantedKeys) > 0 { return errs.BadValue{What: "$unix:rlimits", Valid: "map containing all resource keys", Actual: vals.ReprPlain(val)} } return nil } func parseRlimitMap(val any) (*unix.Rlimit, error) { if err := checkRlimitMapKeys(val); err != nil { return nil, err } cur, err := indexRlimitMap(val, "cur") if err != nil { return nil, err } max, err := indexRlimitMap(val, "max") if err != nil { return nil, err } return &unix.Rlimit{Cur: cur, Max: max}, nil } func checkRlimitMapKeys(val any) error { var errKey error err := vals.IterateKeys(val, func(k any) bool { if k != "cur" && k != "max" { errKey = errs.BadValue{What: "key of rlimit value", Valid: "cur or max", Actual: vals.ReprPlain(k)} return false } return true }) if err != nil { return errs.BadValue{What: "rlimit value", Valid: "map", Actual: vals.Kind(val)} } return errKey } func indexRlimitMap(m any, key string) (rlimT, error) { val, err := vals.Index(m, key) if err != nil { return unix.RLIM_INFINITY, nil } if r, ok := parseRlimT(val); ok { return r, nil } return 0, errs.BadValue{What: key + " in rlimit value", Valid: rlimTValid, Actual: vals.ReprPlain(val)} } elvish-0.21.0/pkg/mods/unix/rlimit_keys.go000066400000000000000000000023671465720375400205010ustar00rootroot00000000000000//go:build unix package unix import "golang.org/x/sys/unix" var rlimitKeys = map[int]string{ // The following are defined by POSIX // (https://pubs.opengroup.org/onlinepubs/9699919799/functions/getrlimit.html). // // Note: RLIMIT_AS is defined by POSIX, but missing on OpenBSD // (https://man.openbsd.org/getrlimit.2#BUGS); it is defined on Darwin, but // it's an undocumented alias of RLIMIT_RSS there. unix.RLIMIT_CORE: "core", unix.RLIMIT_CPU: "cpu", unix.RLIMIT_DATA: "data", unix.RLIMIT_FSIZE: "fsize", unix.RLIMIT_NOFILE: "nofile", unix.RLIMIT_STACK: "stack", // The following are not defined by POSIX, but supported by every Unix OS // Elvish supports (Linux, macOS, Free/Net/OpenBSD). See: // // - https://man7.org/linux/man-pages/man2/setrlimit.2.html // - https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrlimit.2.html // - https://www.freebsd.org/cgi/man.cgi?query=getrlimit // - https://man.netbsd.org/getrlimit.2 // - https://man.openbsd.org/getrlimit.2 unix.RLIMIT_MEMLOCK: "memlock", unix.RLIMIT_NPROC: "nproc", unix.RLIMIT_RSS: "rss", } //lint:ignore U1000 used on some OS func addRlimitKeys(m map[int]string) { for k, v := range m { rlimitKeys[k] = v } } elvish-0.21.0/pkg/mods/unix/rlimit_keys_as.go000066400000000000000000000002401465720375400211500ustar00rootroot00000000000000//go:build linux || freebsd || netbsd package unix import "golang.org/x/sys/unix" func init() { addRlimitKeys(map[int]string{ unix.RLIMIT_AS: "as", }) } elvish-0.21.0/pkg/mods/unix/rlimit_keys_linux.go000066400000000000000000000005671465720375400217200ustar00rootroot00000000000000package unix import "golang.org/x/sys/unix" func init() { // https://man7.org/linux/man-pages/man2/setrlimit.2.html addRlimitKeys(map[int]string{ unix.RLIMIT_LOCKS: "locks", unix.RLIMIT_MSGQUEUE: "msgqueue", unix.RLIMIT_NICE: "nice", unix.RLIMIT_RTPRIO: "rtprio", unix.RLIMIT_RTTIME: "rttime", unix.RLIMIT_SIGPENDING: "sigpending", }) } elvish-0.21.0/pkg/mods/unix/rlimit_keys_netbsd.go000066400000000000000000000004441465720375400220320ustar00rootroot00000000000000package unix func init() { // https://man.netbsd.org/getrlimit.2 // // RLIMIT_NTHR and RLIMIT_SBSIZE are missing from x/sys/unix; the values are // taken from https://github.com/NetBSD/src/blob/trunk/sys/sys/resource.h. addRlimitKeys(map[int]string{ 11: "nthr", 9: "sbsize", }) } elvish-0.21.0/pkg/mods/unix/rlimit_test.elvts000066400000000000000000000050711465720375400212300ustar00rootroot00000000000000//each:eval use unix //each:mock-rlimit /////////// # reading # /////////// ~> put $unix:rlimits[cpu] ▶ [&] ~> put $unix:rlimits[nofile] ▶ [&cur=(num 30) &max=(num 40)] ~> has-key $unix:rlimits stack ▶ $false /////////// # setting # /////////// ~> set unix:rlimits[cpu] = [&cur=3 &max=8] put $cpu-cur $cpu-max ▶ (num 3) ▶ (num 8) ~> set unix:rlimits[cpu] = [&cur=4] put $cpu-cur $cpu-max ▶ (num 4) ▶ (num -1) ~> set unix:rlimits[cpu] = [&] put $cpu-cur $cpu-max ▶ (num -1) ▶ (num -1) ~> set unix:rlimits[nofile] = [&] Exception: setrlimit nofile: fake setrlimit error [tty]:1:5-24: set unix:rlimits[nofile] = [&] ////////////// # bad values # ////////////// ## bad rlimits value ## ~> set unix:rlimits = x Exception: bad value: $unix:rlimits must be map, but is string [tty]:1:5-16: set unix:rlimits = x ~> set unix:rlimits = [&[]=[&]] Exception: bad value: key of $unix:rlimits must be string, but is list [tty]:1:5-16: set unix:rlimits = [&[]=[&]] ~> set unix:rlimits = [&bad-resource=[&]] Exception: bad value: key of $unix:rlimits must be valid resource key, but is bad-resource [tty]:1:5-16: set unix:rlimits = [&bad-resource=[&]] ~> set unix:rlimits = [&] Exception: bad value: $unix:rlimits must be map containing all resource keys, but is [&] [tty]:1:5-16: set unix:rlimits = [&] ## bad map value ## ~> set unix:rlimits[cpu] = x Exception: bad value: rlimit value must be map, but is string [tty]:1:5-21: set unix:rlimits[cpu] = x ~> set unix:rlimits[cpu] = [&bad] Exception: bad value: key of rlimit value must be cur or max, but is bad [tty]:1:5-21: set unix:rlimits[cpu] = [&bad] ## limit out of range (non-FreeBSD) ## //only-on !freebsd ~> set unix:rlimits[cpu] = [&cur=[]] Exception: bad value: cur in rlimit value must be number between 0 and 18446744073709551615, but is [] [tty]:1:5-21: set unix:rlimits[cpu] = [&cur=[]] ~> set unix:rlimits[cpu] = [&cur=1 &max=[]] Exception: bad value: max in rlimit value must be number between 0 and 18446744073709551615, but is [] [tty]:1:5-21: set unix:rlimits[cpu] = [&cur=1 &max=[]] ## limit out of range (FreeBSD) ## //only-on freebsd // FreeBSD uses int64 for the limit values, so the error message is different. ~> set unix:rlimits[cpu] = [&cur=[]] Exception: bad value: cur in rlimit value must be number between 0 and 9223372036854775807, but is [] [tty]:1:5-21: set unix:rlimits[cpu] = [&cur=[]] ~> set unix:rlimits[cpu] = [&cur=1 &max=[]] Exception: bad value: max in rlimit value must be number between 0 and 9223372036854775807, but is [] [tty]:1:5-21: set unix:rlimits[cpu] = [&cur=1 &max=[]] elvish-0.21.0/pkg/mods/unix/testexport_test.go000066400000000000000000000001531465720375400214150ustar00rootroot00000000000000//go:build unix package unix type RlimT = rlimT var ( GetRlimit = &getRlimit SetRlimit = &setRlimit ) elvish-0.21.0/pkg/mods/unix/umask.d.elv000066400000000000000000000022641465720375400176650ustar00rootroot00000000000000# The [file mode creation mask](https://en.wikipedia.org/wiki/Umask) for the # process. # # Bits that are set in the mask get **cleared** in the actual permission of # newly created files: for example, a value of `0o022` causes the group write # and world write bits to be cleared when creating a new file. # # This variable has some special properties when read or assigned: # # - When read, the value is a string in octal representation, like `0o027`. # # - When assigned, both strings that parse as numbers and typed numbers may be # specified as the new value, as long as the value is within the range [0, # 0o777]. # # As a special case for this variable, strings that don't start with `0b` or # `0x` are treated as octal instead of decimal: for example, `set unix:umask # = 27` is equivalent to `set unix:umask = 0o27` or `set unix:umask = (num # 0o27)`, and **not** the same as `set unix:umask = (num 27)`. # # You can do a temporary assignment to affect a single command, like # `{ tmp umask = 077; touch a_file }`, but beware that since umask applies to # the whole process, any code that runs in parallel (such as via # [`peach`]()) can also get affected. var umask elvish-0.21.0/pkg/mods/unix/umask.go000066400000000000000000000057421465720375400172660ustar00rootroot00000000000000//go:build unix package unix import ( "fmt" "math" "math/big" "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]" ) // 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{} // There is no way to query the current umask without changing it, so we store // the umask value in a variable, and initialize it during startup. It needs to // be mutex-guarded since it could be read or written concurrently. // // This assumes no other part of the Elvish code base involved in the // interpreter ever calls unix.Umask, which is guaranteed by the // check-content.sh script. var ( umaskVal int umaskMutex sync.RWMutex ) func init() { // Init functions are run concurrently, so it's normally impossible to // observe the temporary value. // // Even if there is some pathological init logic (e.g. goroutine from init // functions), the failure pattern is relative safe because we are setting // the temporary umask to the most restrictive value possible. umask := unix.Umask(0o777) unix.Umask(umask) umaskVal = umask } // Get returns the current file creation umask as a string. func (UmaskVariable) Get() any { umaskMutex.RLock() defer umaskMutex.RUnlock() return fmt.Sprintf("0o%03o", umaskVal) } // Set changes the current file creation umask. It can be called with a string // or a number. Strings are treated as octal numbers by default, unless they // have an explicit base prefix like 0x or 0b. func (UmaskVariable) Set(v any) error { umask, err := parseUmask(v) if err != nil { return err } umaskMutex.Lock() defer umaskMutex.Unlock() unix.Umask(umask) umaskVal = umask return nil } func parseUmask(v any) (int, 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 -1, errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)} } } umask = int(i) case int: umask = v case float64: intPart, fracPart := math.Modf(v) if fracPart != 0 { return -1, errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)} } umask = int(intPart) case *big.Int: return -1, errs.OutOfRange{ What: "umask", ValidLow: "0", ValidHigh: "0o777", Actual: vals.ToString(v)} case *big.Rat: return -1, errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)} default: return -1, errs.BadValue{ What: "umask", Valid: validUmaskMsg, Actual: vals.Kind(v)} } if umask < 0 || umask > 0o777 { return -1, errs.OutOfRange{ What: "umask", ValidLow: "0", ValidHigh: "0o777", Actual: fmt.Sprintf("%O", umask)} } return umask, nil } elvish-0.21.0/pkg/mods/unix/umask_test.elvts000066400000000000000000000045161465720375400210530ustar00rootroot00000000000000//each:eval use unix /////////// # parsing # /////////// ## implicit octal ## ~> set unix:umask = 23 put $unix:umask ▶ 0o023 ## explicit base prefixes ## ~> set unix:umask = 0o75 put $unix:umask ▶ 0o075 ~> set unix:umask = 0x43 put $unix:umask ▶ 0o103 ~> set unix:umask = 0b001010100; put $unix:umask ▶ 0o124 ## typed number ## ~> set unix:umask = (num 0o123) put $unix:umask ▶ 0o123 // inexact integers are also supported ~> set unix:umask = (num 9.0) put $unix:umask ▶ 0o011 /////////////////////////////// # effect on external commands # /////////////////////////////// // The output of umask is unspecified in POSIX, but all Unix flavors Elvish // supports write a 0 followed by an octal number. There is one inconsistency // though: OpenBSD does not zero-pad the number (other than the leading 0), so a // umask of 0o012 will appear as 012 on OpenBSD but 0012 on other platforms. // Avoid this by using a umask that is 3 octal digits long. ~> set unix:umask = 0123; sh -c umask 0123 /////////////////// # temp assignment # /////////////////// ~> set unix:umask = 022 { tmp unix:umask = 011; put $unix:umask } put $unix:umask ▶ 0o011 ▶ 0o022 ////////////////// ## parse errors ## ////////////////// ## not integer ## ~> set unix:umask = (num 123.4) Exception: bad value: umask must be integer in the range [0..0o777], but is 123.4 [tty]:1:5-14: set unix:umask = (num 123.4) ~> set unix:umask = (num 1/2) Exception: bad value: umask must be integer in the range [0..0o777], but is 1/2 [tty]:1:5-14: set unix:umask = (num 1/2) ## not number ## ~> set unix:umask = 022z Exception: bad value: umask must be integer in the range [0..0o777], but is 022z [tty]:1:5-14: set unix:umask = 022z ## invalid type ## ~> set unix:umask = [1] Exception: bad value: umask must be integer in the range [0..0o777], but is list [tty]:1:5-14: set unix:umask = [1] ## out of range ## ~> set unix:umask = 0o1000 Exception: out of range: umask must be from 0 to 0o777, but is 0o1000 [tty]:1:5-14: set unix:umask = 0o1000 ~> set unix:umask = -1 Exception: out of range: umask must be from 0 to 0o777, but is -0o1 [tty]:1:5-14: set unix:umask = -1 ~> set unix:umask = (num 100000000000000000000) Exception: out of range: umask must be from 0 to 0o777, but is 100000000000000000000 [tty]:1:5-14: set unix:umask = (num 100000000000000000000) elvish-0.21.0/pkg/mods/unix/umask_test.go000066400000000000000000000022561465720375400203220ustar00rootroot00000000000000//go:build unix package unix import ( "os" "strconv" "sync" "testing" "src.elv.sh/pkg/must" "src.elv.sh/pkg/testutil" ) func TestUmaskGetRace(t *testing.T) { testutil.Umask(t, 0o22) testutil.InTempDir(t) // An old implementation of UmaskVariable.Get had a bug where it will // briefly set the umask to 0, which can impact files created concurrently. for i := 0; i < 100; i++ { filename := strconv.Itoa(i) runParallel( func() { // Calling UmaskVariable.Get is much quicker than creating a // file, so do it in a loop to increase the chance of triggering // a race condition. for j := 0; j < 100; j++ { UmaskVariable{}.Get() } }, func() { must.OK(create(filename, 0o666)) }) perm := must.OK1(os.Stat(filename)).Mode().Perm() if perm != 0o644 { t.Errorf("got perm %o, want 0o644 (run %d)", perm, i) } } } func runParallel(funcs ...func()) { var wg sync.WaitGroup wg.Add(len(funcs)) for _, f := range funcs { f := f go func() { f() wg.Done() }() } wg.Wait() } func create(name string, perm os.FileMode) error { file, err := os.OpenFile(name, os.O_CREATE, perm) if err == nil { file.Close() } return err } elvish-0.21.0/pkg/mods/unix/unix.go000066400000000000000000000013651465720375400171260ustar00rootroot00000000000000//go:build unix // 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" "src.elv.sh/pkg/logutil" ) // 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. var Ns = eval.BuildNs(). AddVars(map[string]vars.Var{ "umask": UmaskVariable{}, "rlimits": rlimitsVar{}, }).Ns() var logger = logutil.GetLogger("[mods/unix] ") elvish-0.21.0/pkg/mods/unix/unix_test.go000066400000000000000000000025771465720375400201730ustar00rootroot00000000000000//go:build unix package unix_test import ( "embed" "errors" "testing" "golang.org/x/sys/unix" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vars" unixmod "src.elv.sh/pkg/mods/unix" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { // Intention is to restore umask after test finishes testutil.Umask(t, 0) evaltest.TestTranscriptsInFS(t, transcripts, "mock-rlimit", mockRlimit, ) } func mockRlimit(t *testing.T, ev *eval.Evaler) { testutil.Set(t, unixmod.GetRlimit, func(res int, lim *unix.Rlimit) error { switch res { case unix.RLIMIT_CPU: *lim = unix.Rlimit{Cur: unix.RLIM_INFINITY, Max: unix.RLIM_INFINITY} case unix.RLIMIT_NOFILE: *lim = unix.Rlimit{Cur: 30, Max: 40} case unix.RLIMIT_STACK: return errors.New("fake getrlimit error") } return nil }) var cpuCur, cpuMax int testutil.Set(t, unixmod.SetRlimit, func(res int, lim *unix.Rlimit) error { switch res { case unix.RLIMIT_CPU: cpuCur = rlimTToInt(lim.Cur) cpuMax = rlimTToInt(lim.Max) case unix.RLIMIT_NOFILE: return errors.New("fake setrlimit error") } return nil }) ev.ExtendGlobal(eval.BuildNs(). AddVar("cpu-cur", vars.FromPtr(&cpuCur)). AddVar("cpu-max", vars.FromPtr(&cpuMax))) } func rlimTToInt(r unixmod.RlimT) int { if r == unix.RLIM_INFINITY { return -1 } return int(r) } elvish-0.21.0/pkg/must/000077500000000000000000000000001465720375400146525ustar00rootroot00000000000000elvish-0.21.0/pkg/must/must.go000066400000000000000000000037331465720375400161770ustar00rootroot00000000000000// Package must contains simple functions that panic on errors. // // It should only be used in tests and rare places where errors are provably // impossible. package must import ( "io" "os" "path/filepath" ) // OK panics if the error value is not nil. It is intended for use with // functions that return just an error. func OK(err error) { if err != nil { panic(err) } } // OK1 panics if the error value is not nil. It is intended for use with // functions that return one value and an error. func OK1[T any](v T, err error) T { if err != nil { panic(err) } return v } // OK2 panics if the error value is not nil. It is intended for use with // functions that return two values and an error. func OK2[T1, T2 any](v1 T1, v2 T2, err error) (T1, T2) { if err != nil { panic(err) } return v1, v2 } // Pipe wraps os.Pipe. func Pipe() (*os.File, *os.File) { return OK2(os.Pipe()) } // Chdir wraps os.Chdir. func Chdir(dir string) { OK(os.Chdir(dir)) } // ReadAll wraps io.ReadAll and io.Closer.Close. func ReadAllAndClose(r io.ReadCloser) []byte { v := OK1(io.ReadAll(r)) OK(r.Close()) return v } // ReadFile wraps os.ReadFile. func ReadFile(fname string) []byte { return OK1(os.ReadFile(fname)) } // ReadFileString converts the result of ReadFile to a string. func ReadFileString(fname string) string { return string(ReadFile(fname)) } // MkdirAll calls os.MkdirAll for each argument. func MkdirAll(names ...string) { for _, name := range names { OK(os.MkdirAll(name, 0700)) } } // CreateEmpty creates empty file, after creating all ancestor directories that // don't exist. func CreateEmpty(names ...string) { for _, name := range names { OK(os.MkdirAll(filepath.Dir(name), 0700)) file := OK1(os.Create(name)) OK(file.Close()) } } // WriteFile writes data to a file, after creating all ancestor directories that // don't exist. func WriteFile(filename, data string) { OK(os.MkdirAll(filepath.Dir(filename), 0700)) OK(os.WriteFile(filename, []byte(data), 0600)) } elvish-0.21.0/pkg/parse/000077500000000000000000000000001465720375400147745ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/check_ast_test.go000066400000000000000000000105001465720375400203020ustar00rootroot00000000000000package 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]any // 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 any, want any, 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 any) 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.21.0/pkg/parse/check_parse_tree_test.go000066400000000000000000000022371465720375400216540ustar00rootroot00000000000000package 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.21.0/pkg/parse/cmpd/000077500000000000000000000000001465720375400157175ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/cmpd/cmpd.go000066400000000000000000000034551465720375400172000ustar00rootroot00000000000000// 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 n != nil && 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.21.0/pkg/parse/cmpd/cmpd_test.go000066400000000000000000000002011465720375400202210ustar00rootroot00000000000000package cmpd import "testing" func Test(t *testing.T) { // Test coverage of this package is provided by tests of its users. } elvish-0.21.0/pkg/parse/fuzz_test.go000066400000000000000000000023011465720375400173540ustar00rootroot00000000000000package parse import ( "testing" "unicode/utf8" "src.elv.sh/pkg/diag" ) func FuzzParse_CrashOrVerySlow(f *testing.F) { f.Add("echo") f.Add("put $x") f.Add("put foo bar | each {|x| echo $x }") f.Fuzz(func(t *testing.T, code string) { Parse(Source{Name: "fuzz", Code: code}, Config{}) }) } func FuzzPartialError(f *testing.F) { for _, test := range testCases { f.Add(test.code) } fuzzPartialError(f, func(src Source) []*Error { _, err := Parse(src, Config{}) return UnpackErrors(err) }) } func fuzzPartialError[T diag.ErrorTag](f *testing.F, fn func(src Source) []*diag.Error[T]) { f.Fuzz(func(t *testing.T, code string) { if !utf8.ValidString(code) { t.SkipNow() } errs := fn(Source{Name: "fuzz.elv", Code: code}) if len(errs) > 0 { t.SkipNow() } // If code has no error, then every prefix of it (as long as it's valid // UTF-8) should have either no errors or only partial errors. for i := range code { if i == 0 { continue } prefix := code[:i] errs := fn(Source{Name: "fuzz.elv", Code: prefix}) for _, err := range errs { if !err.Partial { t.Errorf("prefix %q of valid %q has non-partial error: %v", prefix, code, err) } } } }) } elvish-0.21.0/pkg/parse/node.go000066400000000000000000000016141465720375400162520ustar00rootroot00000000000000package 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.21.0/pkg/parse/np/000077500000000000000000000000001465720375400154115ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/np/np.go000066400000000000000000000070161465720375400163610ustar00rootroot00000000000000// Package np provides utilities for working with node paths from a leaf of a // parse tree to the root. package np import ( "src.elv.sh/pkg/eval" "src.elv.sh/pkg/parse" ) // Path is a path from a leaf in a parse tree to the root. type Path []parse.Node // Find finds the path of nodes from the leaf at position p to the root. func Find(root parse.Node, p int) Path { return find(root, p, false) } // FindLeft finds the path of nodes from the leaf at position p to the root. If // p points to the start of one node (p == x.From), FindLeft finds the node to // the left instead (y s.t. p == y.To). func FindLeft(root parse.Node, p int) Path { return find(root, p, true) } func find(root parse.Node, p int, preferLeft bool) Path { n := root descend: for len(parse.Children(n)) > 0 { for _, ch := range parse.Children(n) { r := ch.Range() if r.From <= p && p < r.To || preferLeft && p == r.To { n = ch continue descend } } return nil } var path []parse.Node for { path = append(path, n) if n == root { break } n = parse.Parent(n) } return path } // Match matches against matchers, and returns whether all matches have // succeeded. func (p Path) Match(ms ...Matcher) bool { for _, m := range ms { p2, ok := m.Match(p) if !ok { return false } p = p2 } return true } // Matcher wraps the Match method. type Matcher interface { // Match takes a slice of nodes and returns the remaining nodes and whether // the match succeeded. Match([]parse.Node) ([]parse.Node, bool) } // Typed returns a [Matcher] matching one node of a given type. func Typed[T parse.Node]() Matcher { return typedMatcher[T]{} } // Commonly used [Typed] matchers. var ( Chunk = Typed[*parse.Chunk]() Pipeline = Typed[*parse.Pipeline]() Array = Typed[*parse.Array]() Redir = Typed[*parse.Redir]() Sep = Typed[*parse.Sep]() ) type typedMatcher[T parse.Node] struct{} func (m typedMatcher[T]) Match(ns []parse.Node) ([]parse.Node, bool) { if len(ns) > 0 { if _, ok := ns[0].(T); ok { return ns[1:], true } } return nil, false } // Store returns a [Matcher] matching one node of a given type, and stores it // if a match succeeds. func Store[T parse.Node](p *T) Matcher { return storeMatcher[T]{p} } type storeMatcher[T parse.Node] struct{ p *T } func (m storeMatcher[T]) Match(ns []parse.Node) ([]parse.Node, bool) { if len(ns) > 0 { if n, ok := ns[0].(T); ok { *m.p = n return ns[1:], true } } return nil, false } // SimpleExpr returns a [Matcher] matching a "simple expression", which consists // of 3 nodes from the leaf upwards (Primary, Indexing and Compound) and where // the Compound expression can be evaluated statically using ev. func SimpleExpr(data *SimpleExprData, ev *eval.Evaler) Matcher { return simpleExprMatcher{data, ev} } // SimpleExprData contains useful data written by the [SimpleExpr] matcher. type SimpleExprData struct { Value string Compound *parse.Compound PrimarType parse.PrimaryType } type simpleExprMatcher struct { data *SimpleExprData ev *eval.Evaler } func (m simpleExprMatcher) Match(ns []parse.Node) ([]parse.Node, bool) { if len(ns) < 3 { return nil, false } primary, ok := ns[0].(*parse.Primary) if !ok { return nil, false } indexing, ok := ns[1].(*parse.Indexing) if !ok { return nil, false } compound, ok := ns[2].(*parse.Compound) if !ok { return nil, false } value, ok := m.ev.PurelyEvalPartialCompound(compound, indexing.To) if !ok { return nil, false } *m.data = SimpleExprData{value, compound, primary.Type} return ns[3:], true } elvish-0.21.0/pkg/parse/np/np_test.go000066400000000000000000000001771465720375400174210ustar00rootroot00000000000000package np import "testing" func Test(t *testing.T) { // Test coverage of this package is provided by tests of its users. } elvish-0.21.0/pkg/parse/parse.go000066400000000000000000000566061465720375400164520ustar00rootroot00000000000000/* Package parse implements parsing of Elvish code. The entrypoint of this package is [Parse] and the more low-level [ParseAs]. This package defines many types that implement the [Node] interface, [Chunk] being the root node. Each node can be thought of as a AST/parse tree hybrid: - To access the semantically relevant information of a node (as an AST), use its exported fields. - To access all its children (as a parse tree), call [Children]. Internally, this package uses a handwritten recursive-descent parser. There's no separate tokenization phase; the parser consumes the source text directly. */ package parse //go:generate stringer -type=PrimaryType,RedirMode,ExprCtx -output=zstring.go import ( "bytes" "io" "math" "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 may contain one or more // parse error, which can be unpacked with [UnpackErrors]. 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. The returned error may contain one or more parse error, which can be // unpacked with [UnpackErrors]. func ParseAs(src Source, n Node, cfg Config) error { ps := &parser{srcName: src.Name, src: src.Code, warn: cfg.WarningWriter} parse(ps, n) ps.done() return diag.PackErrors(ps.errors) } // 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") errInvalidEscapeOctOverflow = newError("invalid octal escape sequence", "below 256") 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") 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()) { parse(ps, &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) { parse(ps, &Form{}).addTo(&pn.Forms, pn) for parseSep(pn, ps, '|') { parseSpacesAndNewlines(pn, ps) if !startsForm(ps.peek()) { ps.error(errShouldBeForm) return } parse(ps, &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 = { Compound-CmdExpr } { Space } { ( Compound | MapPair | Redir ) { Space } } type Form struct { node Head *Compound Args []*Compound Opts []*MapPair Redirs []*Redir } func (fn *Form) parse(ps *parser) { parse(ps, &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 } parse(ps, &MapPair{}).addTo(&fn.Opts, fn) case startsCompound(r, NormalExpr): cn := &Compound{} parse(ps, cn) if isRedirSign(ps.peek()) { // Redir parse(ps, &Redir{Left: cn}).addTo(&fn.Redirs, fn) } else { fn.Args = append(fn.Args, cn) addChild(fn, cn) } case isRedirSign(r): parse(ps, &Redir{}).addTo(&fn.Redirs, fn) default: return } parseSpaces(fn, ps) } } func startsForm(r rune) bool { return IsInlineWhitespace(r) || startsCompound(r, CmdExpr) } // 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 } parse(ps, &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 == '&': parse(ps, &MapPair{}).addTo(&qn.Opts, qn) case startsCompound(r, NormalExpr): parse(ps, &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) { parse(ps, &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} in.Head = pn addChild(in, pn) cn.Indexings = append(cn.Indexings, in) addChild(cn, in) } } 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) { parse(ps, &Primary{ExprCtx: in.ExprCtx}).addAs(&in.Head, in) for parseSep(in, ps, '[') { if !startsArray(ps.peek()) && ps.peek() != ']' { ps.error(errShouldBeArray) } parse(ps, &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) { parse(ps, &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 // 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 } if r == 'x' { buf.WriteByte(byte(rr)) } else { 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') } if rr <= math.MaxUint8 { buf.WriteByte(byte(rr)) } else { r := diag.Ranging{From: ps.pos - 4, To: ps.pos} ps.errorp(r, errInvalidEscapeOctOverflow) } 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() } } } // Keep this consistent with the (*Primary).variable above. // ValidLHSVariable returns whether a [Primary] node containing a variable name // being used as the LHS of an assignment form without the $ prefix is valid. func ValidLHSVariable(p *Primary, allowSigil bool) bool { switch p.Type { case SingleQuoted, DoubleQuoted: // Quoted variable names may contain anything return true case Bareword: // Bareword LHS variable are only allowed if they are also valid after a // $, even if they are valid barewords. For example, a variable named // a/b must be quoted after $ (as $'a/b'), so for consistency, we also // require it to be quoted after set (like set 'a/b' = foo) even if a/b // is a valid bareword. name := p.Value if name == "" { return false } if allowSigil && name[0] == '@' { name = name[1:] } for _, r := range name { if !allowedInVariableName(r) { return false } } return true default: return false } } // 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 parse(ps, &Chunk{}).addAs(&pn.Chunk, pn) if !parseSep(pn, ps, ')') { ps.error(errShouldBeRParen) } } func (pn *Primary) outputCapture(ps *parser) { pn.Type = OutputCapture parseSep(pn, ps, '(') parse(ps, &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() parse(ps, &MapPair{}).addTo(&pn.MapPairs, pn) case startsCompound(r, NormalExpr): parse(ps, &Compound{}).addTo(&pn.Elements, pn) default: break items } parseSpacesAndNewlines(pn, ps) } if !parseSep(pn, ps, ']') { ps.error(errShouldBeRBracket) } 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 parseSpacesAndNewlines(pn, ps) if parseSep(pn, ps, '|') { parseSpacesAndNewlines(pn, ps) items: for { r := ps.peek() switch { case r == '&': parse(ps, &MapPair{}).addTo(&pn.MapPairs, pn) case startsCompound(r, NormalExpr): parse(ps, &Compound{}).addTo(&pn.Elements, pn) default: break items } parseSpacesAndNewlines(pn, ps) } if !parseSep(pn, ps, '|') { ps.error(errShouldBePipe) } } parse(ps, &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. parse(ps, &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) parse(ps, &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, '&') parse(ps, &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. parse(ps, &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' } elvish-0.21.0/pkg/parse/parse_test.go000066400000000000000000000414101465720375400174740ustar00rootroot00000000000000package parse import ( "fmt" "os" "testing" ) func a(c ...any) 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: "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"}}}, }, { name: "indexing expression with empty index", code: "$a[]", node: &Indexing{}, want: ast{"Indexing", fs{ "Head": "$a", "Indices": []string{""}}}, }, // 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 for codepoints", code: `"b\^[\u548c\U0002CE23\n\t\\"`, node: &Primary{}, want: ast{"Primary", fs{ "Type": DoubleQuoted, "Value": "b\x1b\u548c\U0002CE23\n\t\\", }}, }, { name: "double-quoted string with numerical escape sequences for bytes", code: `"\123\321 \x7f\xff"`, node: &Primary{}, want: ast{"Primary", fs{ "Type": DoubleQuoted, "Value": "\123\321 \x7f\xff", }}, }, { 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 without signature", code: "{ echo}", node: &Primary{}, want: ast{"Primary", fs{ "Type": Lambda, "Chunk": "echo", }}, }, { name: "new-style lambda with arguments and options", code: "{|a b &k=v| echo}", node: &Primary{}, want: ast{"Primary", fs{ "Type": Lambda, "Elements": []string{"a", "b"}, "MapPairs": []string{"&k=v"}, "Chunk": " echo", }}, }, { 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: "overflow in octal escape sequence", code: `a "\400"`, node: &Chunk{}, wantErrPart: "\\400", wantErrMsg: "invalid octal escape sequence, should be below 256", }, { 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 := UnpackErrors(err)[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) } } func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { for _, test := range testCases { _ = ParseAs(SourceForTest(test.code), test.node, Config{}) } } } elvish-0.21.0/pkg/parse/parser.go000066400000000000000000000054471465720375400166310ustar00rootroot00000000000000package parse import ( "bytes" "errors" "fmt" "io" "strings" "unicode/utf8" "src.elv.sh/pkg/diag" ) // parser maintains some mutable states of parsing. // // NOTE: The src member is assumed to be valid UF-8. type parser struct { srcName string src string pos int overEOF int errors []*Error warn io.Writer } // Error is a parse error. type Error = diag.Error[ErrorTag] // ErrorTag parameterizes [diag.Error] to define [Error]. type ErrorTag struct{} func (ErrorTag) ErrorTag() string { return "parse error" } func parse[N Node](ps *parser, n N) parsed[N] { 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]{n} } type parsed[N Node] struct { n N } func (p parsed[N]) addAs(ptr *N, parent Node) { *ptr = p.n addChild(parent, p.n) } func (p parsed[N]) addTo(ptr *[]N, parent Node) { *ptr = append(*ptr, p.n) addChild(parent, p.n) } func addChild(p Node, ch Node) { p.n().addChild(ch) ch.n().parent = p } // 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)) } } 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) { err := &Error{ Message: e.Error(), Context: *diag.NewContext(ps.srcName, ps.src, r), Partial: r.Range().From == len(ps.src), } ps.errors = append(ps.errors, err) } 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) } // UnpackErrors returns the constituent parse errors if the given error contains // one or more parse errors. Otherwise it returns nil. func UnpackErrors(e error) []*Error { if errs := diag.UnpackErrors[ErrorTag](e); len(errs) > 0 { return errs } return nil } 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.21.0/pkg/parse/parseutil/000077500000000000000000000000001465720375400170045ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/parseutil/parseutil.go000066400000000000000000000014101465720375400213370ustar00rootroot00000000000000// Package parseutil contains utilities built on top of the parse package. package parseutil import ( "strings" "src.elv.sh/pkg/parse" ) // Wordify turns a piece of source code into words. func Wordify(src string) []string { tree, _ := parse.Parse(parse.Source{Name: "[unknown]", 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.21.0/pkg/parse/parseutil/parseutil_test.go000066400000000000000000000001641465720375400224030ustar00rootroot00000000000000package parseutil import "testing" func Test(t *testing.T) { // Required to get accurate test coverage report. } elvish-0.21.0/pkg/parse/pprint.go000066400000000000000000000064001465720375400166370ustar00rootroot00000000000000package 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 any } 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.21.0/pkg/parse/pprint_test.go000066400000000000000000000044451465720375400177050ustar00rootroot00000000000000package parse import ( "strings" "testing" "src.elv.sh/pkg/tt" ) var n = mustParse("ls $x[0]$y[1];echo done >/redir-dest") var pprintASTTests = []*tt.Case{ Args(n).Rets( `Chunk Pipeline/Form Compound/Indexing/Primary ExprCtx=CmdExpr Type=Bareword Value="ls" Compound ExprCtx=NormalExpr Indexing ExprCtx=NormalExpr Primary ExprCtx=NormalExpr Type=Variable Value="x" Array/Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword Value="0" Indexing ExprCtx=NormalExpr Primary ExprCtx=NormalExpr Type=Variable Value="y" Array/Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword Value="1" Pipeline/Form Compound/Indexing/Primary ExprCtx=CmdExpr Type=Bareword Value="echo" Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword Value="done" Redir Mode=Write RightIsFd=false Compound/Indexing/Primary ExprCtx=NormalExpr Type=Bareword Value="/redir-dest" `), } func TestPPrintAST(t *testing.T) { pprintAST := func(n Node) string { var b strings.Builder pprintAST(n, &b) return b.String() } tt.Test(t, tt.Fn(pprintAST).Named("pprintAST"), pprintASTTests...) } var pprintParseTreeTests = []*tt.Case{ 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) { pprintParseTree := func(n Node) string { var b strings.Builder pprintParseTree(n, &b) return b.String() } tt.Test(t, tt.Fn(pprintParseTree).Named("pprintParseTree"), pprintParseTreeTests...) } func mustParse(src string) Node { tree, err := Parse(SourceForTest(src), Config{}) if err != nil { panic(err) } return tree.Root } elvish-0.21.0/pkg/parse/quote.go000066400000000000000000000073531465720375400164700ustar00rootroot00000000000000package parse import ( "bytes" "unicode" "unicode/utf8" ) // 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 r == unicode.ReplacementChar || !unicode.IsPrint(r) { // Contains invalid UTF-8 sequence or unprintable character; force // double quote. return quoteDouble(s) } if !allowedInVariableName(r) { bare = false } } if bare { return s } return quoteSingle(s) } // QuoteCommandName is like [Quote], but uses the slightly laxer rule for what // can appear in a command name unquoted, like <. func QuoteCommandName(s string) string { q, _ := quoteAs(s, Bareword, CmdExpr) return q } // 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) { return quoteAs(s, q, strictExpr) } func quoteAs(s string, q PrimaryType, ctx ExprCtx) (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 r == unicode.ReplacementChar || !unicode.IsPrint(r) { // Contains invalid UTF-8 sequence or unprintable character; force // double quote. return quoteDouble(s), DoubleQuoted } if !allowedInBareword(r, ctx) { 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() } // rtohex is optimized for the common cases encountered when encoding Elvish strings and should be // more efficient than using fmt.Sprintf("%x"). 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 s != "" { r, w := utf8.DecodeRuneInString(s) if r == utf8.RuneError && w == 1 { // An invalid UTF-8 sequence was seen -- encode first byte as a hex literal. buf.WriteByte('\\') buf.WriteByte('x') buf.Write(rtohex(rune(s[0]), 2)) } else if e, ok := doubleUnescape[r]; ok { // This handles the escaping of " and \ too. buf.WriteByte('\\') buf.WriteRune(e) } else if unicode.IsPrint(r) && r != utf8.RuneError { // RuneError is technically printable, but don't print it directly // to avoid confusion. buf.WriteRune(r) } else if r <= 0x7f { // Unprintable characters in the ASCII range can be escaped with \x // since they are one byte in UTF-8. buf.WriteByte('\\') buf.WriteByte('x') buf.Write(rtohex(r, 2)) } else if r <= 0xffff { buf.WriteByte('\\') buf.WriteByte('u') buf.Write(rtohex(r, 4)) } else { buf.WriteByte('\\') buf.WriteByte('U') buf.Write(rtohex(r, 8)) } s = s[w:] } buf.WriteByte('"') return buf.String() } elvish-0.21.0/pkg/parse/quote_test.go000066400000000000000000000044321465720375400175220ustar00rootroot00000000000000package parse import ( "testing" "src.elv.sh/pkg/tt" ) func TestQuote(t *testing.T) { tt.Test(t, tt.Fn(Quote).ArgsFmt("(%q)"), // 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("\x7f").Rets(`"\x7f"`), Args("\u0090").Rets(`"\u0090"`), Args("\u0600").Rets(`"\u0600"`), // Arabic number sign Args("\ufffd").Rets(`"\ufffd"`), // Unicode replacement character Args("\U000110BD").Rets(`"\U000110bd"`), // Kathi number sign // String containing characters that can be single-quoted are // double-quoted when it also contains unprintable characters. Args("$\n").Rets(`"$\n"`), // 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'`), // Double quote strings containing invalid UTF-8 sequences with \x. Args("bad\xffUTF-8").Rets(`"bad\xffUTF-8"`), ) } func TestQuoteAs(t *testing.T) { tt.Test(t, tt.Fn(QuoteAs).ArgsFmt("(%q, %s)"), // 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) { tt.Test(t, tt.Fn(QuoteVariableName).ArgsFmt("(%q)"), Args("").Rets("''"), Args("foo").Rets("foo"), Args("a/b").Rets("'a/b'"), Args("\x1b").Rets(`"\e"`), Args("bad\xffUTF-8").Rets(`"bad\xffUTF-8"`), Args("$\n").Rets(`"$\n"`), ) } func TestQuoteCommandName(t *testing.T) { tt.Test(t, tt.Fn(QuoteCommandName).ArgsFmt("(%q)"), Args("<").Rets("<"), Args("foo").Rets("foo"), Args("$").Rets(`'$'`), ) } elvish-0.21.0/pkg/parse/source.go000066400000000000000000000006571465720375400166330ustar00rootroot00000000000000package parse // 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() {} elvish-0.21.0/pkg/parse/source_test.go000066400000000000000000000006431465720375400176650ustar00rootroot00000000000000package parse_test import ( "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) func TestSourceAsStructMap(t *testing.T) { vals.TestValue(t, parse.Source{Name: "[tty]", Code: "echo"}). Kind("map"). Repr("[&code=echo &is-file=$false &name='[tty]']"). AllKeys("name", "code", "is-file") vals.TestValue(t, parse.Source{Name: "/etc/rc.elv", Code: "echo", IsFile: true}). Index("is-file", true) } elvish-0.21.0/pkg/parse/testdata/000077500000000000000000000000001465720375400166055ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/testdata/fuzz/000077500000000000000000000000001465720375400176035ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/testdata/fuzz/FuzzParse/000077500000000000000000000000001465720375400215345ustar00rootroot0000000000000020ffbed79ec93b4b3f5872e6a329fee708da4b901c1b9c9ac10912ea7e862c9c000066400000000000000000000000751465720375400326000ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/testdata/fuzz/FuzzParsego test fuzz v1 string("(((((((((((((((((((((((((((((((($'") cfd8d7d9847d47d7fa94af306484de70f8952db8250aa6ebbf7b1c84afb13a91000066400000000000000000000001431465720375400326060ustar00rootroot00000000000000elvish-0.21.0/pkg/parse/testdata/fuzz/FuzzParsego test fuzz v1 string("((((((((((((((((((===(((((((((((((((\xbd\xf3v\xd4X\x9e\xfd\x89(((((((((0") elvish-0.21.0/pkg/parse/testutil_test.go000066400000000000000000000002031465720375400202320ustar00rootroot00000000000000package parse import ( "reflect" "src.elv.sh/pkg/tt" ) var Args = tt.Args var nodeType = reflect.TypeOf((*Node)(nil)).Elem() elvish-0.21.0/pkg/parse/zstring.go000066400000000000000000000043721465720375400170310ustar00rootroot00000000000000// Code generated by "stringer -type=PrimaryType,RedirMode,ExprCtx -output=zstring.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.21.0/pkg/persistent/000077500000000000000000000000001465720375400160625ustar00rootroot00000000000000elvish-0.21.0/pkg/persistent/.gitignore000066400000000000000000000005251465720375400200540ustar00rootroot00000000000000/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.21.0/pkg/persistent/.travis.yml000066400000000000000000000001451465720375400201730ustar00rootroot00000000000000language: go go: - 1.14 - 1.15 sudo: false os: - linux - osx script: make travis elvish-0.21.0/pkg/persistent/LICENSE000066400000000000000000000260701465720375400170740ustar00rootroot00000000000000Eclipse 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.21.0/pkg/persistent/README.md000066400000000000000000000064621465720375400173510ustar00rootroot00000000000000# Persistent data structure in Go This is a Go clone of Clojure's persistent data structures. 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 5x to 9x as slow. - Read (sequential or random) is about 6x as slow. Benchmarked on an MacBook Air (M1, 2020), with Go 1.17.5: ``` BenchmarkConjNativeN1-8 1779234 673.3 ns/op BenchmarkConjNativeN2-8 948654 1220 ns/op BenchmarkConjNativeN3-8 61242 20138 ns/op BenchmarkConjNativeN4-8 1222 968176 ns/op BenchmarkConjPersistentN1-8 264488 4462 ns/op 6.63x BenchmarkConjPersistentN2-8 119526 9885 ns/op 8.10x BenchmarkConjPersistentN3-8 6760 173995 ns/op 8.64x BenchmarkConjPersistentN4-8 212 5576977 ns/op 5.76x BenchmarkIndexSeqNativeN4-8 32031 37344 ns/op BenchmarkIndexSeqPersistentN4-8 6145 192151 ns/op 5.15x BenchmarkIndexRandNative-8 31366 38016 ns/op BenchmarkIndexRandPersistent-8 5434 216284 ns/op 5.69x BenchmarkEqualNative-8 110090 10738 ns/op BenchmarkEqualPersistent-8 2121 557334 ns/op 51.90x ``` ### 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 MacBook Air (M1, 2020), with Go 1.17.5: ``` goos: darwin goarch: arm64 pkg: src.elv.sh/pkg/persistent/hashmap BenchmarkSequentialConjNative1-8 620540 1900 ns/op BenchmarkSequentialConjNative2-8 22918 52209 ns/op BenchmarkSequentialConjNative3-8 567 2115886 ns/op BenchmarkSequentialConjPersistent1-8 169776 7026 ns/op 3.70x BenchmarkSequentialConjPersistent2-8 3374 354031 ns/op 6.78x BenchmarkSequentialConjPersistent3-8 51 23091870 ns/op 10.91x BenchmarkRandomStringsConjNative1-8 379147 3155 ns/op BenchmarkRandomStringsConjNative2-8 10000 117332 ns/op BenchmarkRandomStringsConjNative3-8 292 4034937 ns/op BenchmarkRandomStringsConjPersistent1-8 96504 12207 ns/op 3.87x BenchmarkRandomStringsConjPersistent2-8 1910 615644 ns/op 5.25x BenchmarkRandomStringsConjPersistent3-8 33 31928604 ns/op 7.91x ``` elvish-0.21.0/pkg/persistent/add-slowdown000077500000000000000000000020551465720375400204140ustar00rootroot00000000000000#!/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. var 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] } var native = [&] each {|line| if (re:match Native $line) { # Remember the result so that it can be used later. var name data = (extract $line) set native[$name] = $data } elif (re:match Persistent $line) { # Calculate slowdown and append to the end of the line. var name data = (extract $line) var native-name = (re:replace Persistent Native $name) if (not (has-key $native $native-name)) { fail 'Native counterpart for '$name' not found' } set line = $line' '(printf '%.2f' (/ $data $native[$native-name]))'x' } echo $line } elvish-0.21.0/pkg/persistent/hash/000077500000000000000000000000001465720375400170055ustar00rootroot00000000000000elvish-0.21.0/pkg/persistent/hash/hash.go000066400000000000000000000017771465720375400202730ustar00rootroot00000000000000// 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.21.0/pkg/persistent/hashmap/000077500000000000000000000000001465720375400175035ustar00rootroot00000000000000elvish-0.21.0/pkg/persistent/hashmap/hashmap.go000066400000000000000000000340111465720375400214520ustar00rootroot00000000000000// 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 any) bool // Hash is the type of a function that returns the hash code of a key. type Hash func(k any) 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 *any equal Equal hash Hash } func (m *hashMap) Len() int { return m.count } func (m *hashMap) Index(k any) (any, 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 any) 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 any) 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 any tail Iterator } func (it *nilVIterator) Elem() (any, any) { 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 any) (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 any, 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 any, 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 any, eq Equal) (any, 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 any, 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 any, 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 any, eq Equal) (any, 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() (any, any) { 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 any value any } 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 any, v1 any, h2 uint32, k2 any, v2 any, 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 any) []mapEntry { newEntries := append([]mapEntry(nil), entries...) newEntries[i] = mapEntry{k, v} return newEntries } func (n *bitmapNode) assoc(shift, hash uint32, k, v any, 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 any, 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 any, eq Equal) (any, 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() (any, any) { 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 any, 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 any, 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 any, eq Equal) (any, bool) { idx := n.findIndex(k, eq) if idx == -1 { return nil, false } return n.entries[idx].value, true } func (n *collisionNode) findIndex(k any, 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() (any, any) { 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.21.0/pkg/persistent/hashmap/hashmap_test.go000066400000000000000000000221401465720375400225110ustar00rootroot00000000000000package hashmap import ( "math/rand" "reflect" "strconv" "testing" "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 any) 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 any) 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) } 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([]any{}, "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 ...any) 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[any]any{} 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 any, 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 []any for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() collected = append(collected, k, v) } wantCollected := []any{nil, "nil value", "k", "v"} if !reflect.DeepEqual(collected, wantCollected) { t.Errorf("collected %v, want %v", collected, wantCollected) } } func BenchmarkSequentialConjNative1(b *testing.B) { nativeSequentialAdd(b.N, N1) } func BenchmarkSequentialConjNative2(b *testing.B) { nativeSequentialAdd(b.N, N2) } func BenchmarkSequentialConjNative3(b *testing.B) { nativeSequentialAdd(b.N, N3) } // nativeSequentialAdd 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 BenchmarkSequentialConjPersistent1(b *testing.B) { sequentialConj(b.N, N1) } func BenchmarkSequentialConjPersistent2(b *testing.B) { sequentialConj(b.N, N2) } func BenchmarkSequentialConjPersistent3(b *testing.B) { sequentialConj(b.N, N3) } // sequentialConj 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 sequentialConj(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 BenchmarkRandomStringsConjNative1(b *testing.B) { nativeRandomStringsAdd(b, N1) } func BenchmarkRandomStringsConjNative2(b *testing.B) { nativeRandomStringsAdd(b, N2) } func BenchmarkRandomStringsConjNative3(b *testing.B) { nativeRandomStringsAdd(b, N3) } // nativeRandomStringsAdd 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 BenchmarkRandomStringsConjPersistent1(b *testing.B) { randomStringsConj(b, N1) } func BenchmarkRandomStringsConjPersistent2(b *testing.B) { randomStringsConj(b, N2) } func BenchmarkRandomStringsConjPersistent3(b *testing.B) { randomStringsConj(b, N3) } func randomStringsConj(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.21.0/pkg/persistent/hashmap/map.go000066400000000000000000000025671465720375400206210ustar00rootroot00000000000000package 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 any) (any, bool) // Assoc returns an almost identical map, with the given key associated with // the given value. Assoc(k, v any) Map // Dissoc returns an almost identical map, with the given key associated // with no value. Dissoc(k any) 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() (any, any) // 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 any) bool { _, ok := m.Index(k) return ok } elvish-0.21.0/pkg/persistent/list/000077500000000000000000000000001465720375400170355ustar00rootroot00000000000000elvish-0.21.0/pkg/persistent/list/list.go000066400000000000000000000012771465720375400203460ustar00rootroot00000000000000// 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 // Conj returns a new list with an additional value in the front. Conj(any) List // First returns the first value in the list. First() any // Rest returns the list after the first value. Rest() List } // Empty is an empty list. var Empty List = &list{} type list struct { first any rest *list count int } func (l *list) Len() int { return l.count } func (l *list) Conj(val any) List { return &list{val, l, l.count + 1} } func (l *list) First() any { return l.first } func (l *list) Rest() List { return l.rest } elvish-0.21.0/pkg/persistent/persistent.go000066400000000000000000000001761465720375400206150ustar00rootroot00000000000000// Package persistent contains subpackages for persistent data structures, // similar to those of Clojure. package persistent elvish-0.21.0/pkg/persistent/vector/000077500000000000000000000000001465720375400173645ustar00rootroot00000000000000elvish-0.21.0/pkg/persistent/vector/vector.go000066400000000000000000000242501465720375400212200ustar00rootroot00000000000000// 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) (any, 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 Conj. Assoc(i int, val any) Vector // Conj returns an almost identical Vector, with an additional element // appended to the end. Conj(val any) 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() any // 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 []any } // 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]any func newNode() node { return node(&[nodeSize]any{}) } func clone(n node) node { a := *n return node(&a) } func nodeFromSlice(s []any) node { var n [nodeSize]any 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) (any, 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) []any { 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 any) Vector { if i < 0 || i > v.count { return nil } else if i == v.count { return v.Conj(val) } if i >= v.treeSize() { newTail := append([]any(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 any) 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) Conj(val any) Vector { // Room in tail? if v.count-v.treeSize() < tailMaxLen { newTail := make([]any, 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, []any{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([]any, 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) (any, 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 any) Vector { if i < 0 || s.begin+i > s.end { return nil } else if s.begin+i == s.end { return s.Conj(val) } return s.v.Assoc(s.begin+i, val).SubVector(s.begin, s.end) } func (s *subVector) Conj(val any) 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() any { 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() any { 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.21.0/pkg/persistent/vector/vector_test.go000066400000000000000000000222561465720375400222630ustar00rootroot00000000000000package vector import ( "errors" "math/rand" "strconv" "testing" ) // 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 TestVector(t *testing.T) { run := func(n int) { t.Run(strconv.Itoa(n), func(t *testing.T) { v := testConj(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.Conj(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) } // testConj creates a vector containing 0...n-1 with Conj, and ensures that the // length of the old and new vectors are expected after each Conj. It returns // the created vector. func testConj(t *testing.T, n int) Vector { v := Empty for i := 0; i < n; i++ { oldv := v v = v.Conj(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 any) { 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.Conj(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.Conj("233"), 1, 2, 3, "233") { t.Errorf("v[0:4].Conj 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.Conj(i) } sv := v.SubVector(64, 65) testIterator(t, sv.Iterator(), 64, 65) } func checkVector(v Vector, values ...any) 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.Conj(elem) v2 = v2.Conj(elem) if !eqVector(v1, v2) { t.Errorf("Not equal after Conj'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 ...any) Vector { v := Empty for _, element := range elements { v = v.Conj(element) } return v } func BenchmarkConjNativeN1(b *testing.B) { benchmarkNativeAppend(b, N1) } func BenchmarkConjNativeN2(b *testing.B) { benchmarkNativeAppend(b, N2) } func BenchmarkConjNativeN3(b *testing.B) { benchmarkNativeAppend(b, N3) } func BenchmarkConjNativeN4(b *testing.B) { benchmarkNativeAppend(b, N4) } func benchmarkNativeAppend(b *testing.B, n int) { for r := 0; r < b.N; r++ { var s []any for i := 0; i < n; i++ { s = append(s, i) } _ = s } } func BenchmarkConjPersistentN1(b *testing.B) { benchmarkConj(b, N1) } func BenchmarkConjPersistentN2(b *testing.B) { benchmarkConj(b, N2) } func BenchmarkConjPersistentN3(b *testing.B) { benchmarkConj(b, N3) } func BenchmarkConjPersistentN4(b *testing.B) { benchmarkConj(b, N4) } func benchmarkConj(b *testing.B, n int) { for r := 0; r < b.N; r++ { v := Empty for i := 0; i < n; i++ { v = v.Conj(i) } } } var ( sliceN4 = make([]any, N4) vectorN4 = Empty ) func init() { for i := 0; i < N4; i++ { vectorN4 = vectorN4.Conj(i) } } var x any 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.Conj(i) v2 = v2.Conj(i) } b.StartTimer() for r := 0; r < b.N; r++ { eq := eqVector(v1, v2) if !eq { panic("not equal") } } } elvish-0.21.0/pkg/pkg.go000066400000000000000000000015531465720375400147760ustar00rootroot00000000000000// Package pkg is the root of packages that implement Elvish. package pkg import "embed" // ElvFiles contains the Elvish sources found inside pkg - the .d.elv files for // modules implemented in Go and the actual sources of bundled modules. It is // defined to support reading documentation of builtin modules from Elvish // itself. // // Some of these files may be embedded as a string variable elsewhere. This is // fine since the compiler is smart enough to only include one copy of the same // file in the binary (as least as of Go 1.21). // // This is only used by [src.elv.sh/pkg/mods/doc], but has to live in this // package because go:embed only supports embedding files found in the current // directory and its descendents, and this directory is the lowest common // ancestor of these files. // //go:embed eval/*.elv edit/*.elv mods/*/*.elv var ElvFiles embed.FS elvish-0.21.0/pkg/pprof/000077500000000000000000000000001465720375400150105ustar00rootroot00000000000000elvish-0.21.0/pkg/pprof/pprof.go000066400000000000000000000025001465720375400164620ustar00rootroot00000000000000// Package pprof adds profiling support to the Elvish program. package pprof import ( "fmt" "os" "runtime/pprof" "src.elv.sh/pkg/prog" ) // Program adds support for the -cpuprofile flag. type Program struct { cpuProfile string allocsProfile string } func (p *Program) RegisterFlags(f *prog.FlagSet) { f.StringVar(&p.cpuProfile, "cpuprofile", "", "write CPU profile to file") f.StringVar(&p.allocsProfile, "allocsprofile", "", "write memory allocation profile to file") } func (p *Program) Run(fds [3]*os.File, _ []string) error { var cleanups []func([3]*os.File) if p.cpuProfile != "" { f, err := os.Create(p.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) cleanups = append(cleanups, func([3]*os.File) { pprof.StopCPUProfile() f.Close() }) } } if p.allocsProfile != "" { f, err := os.Create(p.allocsProfile) if err != nil { fmt.Fprintln(fds[2], "Warning: cannot create memory allocation profile:", err) fmt.Fprintln(fds[2], "Continuing without memory allocation profiling.") } else { cleanups = append(cleanups, func([3]*os.File) { pprof.Lookup("allocs").WriteTo(f, 0) f.Close() }) } } return prog.NextProgram(cleanups...) } elvish-0.21.0/pkg/pprof/pprof_test.elvts000066400000000000000000000012471465720375400202600ustar00rootroot00000000000000//each:elvish-in-global //each:in-temp-dir /////////////// # -cpuprofile # /////////////// ~> elvish -cpuprofile cpu ~> use os > (os:stat cpu)[size] 0 ▶ $true ## bad path ## ~> elvish -cpuprofile bad/cpu &check-stderr-contains='Warning: cannot create CPU profile:' [stderr contains "Warning: cannot create CPU profile:"] true ////////////////// # -allocsprofile # ////////////////// ~> elvish -allocsprofile allocs ~> use os > (os:stat allocs)[size] 0 ▶ $true ## bad path ## ~> elvish -allocsprofile bad/allocs &check-stderr-contains='Warning: cannot create memory allocation profile:' [stderr contains "Warning: cannot create memory allocation profile:"] true elvish-0.21.0/pkg/pprof/transcripts_test.go000066400000000000000000000010511465720375400207470ustar00rootroot00000000000000package pprof_test import ( "embed" "os" "testing" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/pprof" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/prog/progtest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvish-in-global", progtest.ElvishInGlobal( prog.Composite(&pprof.Program{}, noopProgram{})), ) } type noopProgram struct{} func (noopProgram) RegisterFlags(*prog.FlagSet) {} func (noopProgram) Run([3]*os.File, []string) error { return nil } elvish-0.21.0/pkg/prog/000077500000000000000000000000001465720375400146315ustar00rootroot00000000000000elvish-0.21.0/pkg/prog/flags.go000066400000000000000000000020141465720375400162510ustar00rootroot00000000000000package prog import "flag" // FlagSet wraps a [flag.FlagSet], and provides methods to register flags shared // by multiple subprograms on demand. type FlagSet struct { *flag.FlagSet daemonPaths *DaemonPaths json *bool } // DaemonPaths stores the -db and -sock flags. type DaemonPaths struct { DB, Sock string } // DaemonPaths returns a pointer to a struct storing the value of -db and // -sock flags, registering them on demand. func (fs *FlagSet) DaemonPaths() *DaemonPaths { if fs.daemonPaths == nil { var dp DaemonPaths fs.StringVar(&dp.DB, "db", "", "[internal flag] Path to the database file") fs.StringVar(&dp.Sock, "sock", "", "[internal flag] Path to the daemon's Unix socket") fs.daemonPaths = &dp } return fs.daemonPaths } // JSON returns a pointer to the value of the -json flag, registering it on // demand. func (fs *FlagSet) JSON() *bool { if fs.json == nil { fs.json = fs.Bool("json", false, "Show the output from -buildinfo, -compileonly or -version in JSON") } return fs.json } elvish-0.21.0/pkg/prog/prog.go000066400000000000000000000173161465720375400161370ustar00rootroot00000000000000/* Package prog supports building testable, composable programs. The main abstraction of this package is the [Program] interface, which can be combined using [Composite] and run with [Run]. # Testability The easy way to write a Go program is as follows: - Write code in the main function of the main package. - Access process-level context via globals defined in the [os] package, like [os.Args], [os.Stdin], [os.Stdout] and [os.Stderr]. - Call [os.Exit] if the program should exit with a non-zero status. - Declare flags as global variables, with functions like [flag.String]. Programs written this way are hard to test, since they rely on process-level states that are hard or impossible to emulate in tests. With this package, the way to write a Go program is becomes a matter of creating a type that implements the [Program] interface: - Write the "main" function in the Run method. - Context is available as arguments. - Return an error constructed from [Exit] to exit with a non-zero status. - Declare flags as fields of the type, and register them in the RegisterFlags method. The [Program] can be run using the [Run] function, which takes care of parsing flags, calling the Run method, and converting the error return value into an exit code. Since the [Run] function itself takes the standard files and command-line arguments as its function arguments, these can be emulated easily in tests. # Composability Another advantage of this approach is composability. Elvish contains multiple independent subprograms. The default subprogram is the shell; but if you run Elvish with "elvish -buildinfo" or "elvish -daemon", they will invoke the buildinfo or daemon subprogram instead of the shell. Subprograms can also do something in addition to, rather than in place of, other subprograms. One example is profiling support, which declares its own flags like -cpuprofile and runs some extra code before and after other subprograms to start and stop profiling. Using this package, all the different subprograms can be implemented separately and then composed into one using [Composite]. Other than keeping the codebase cleaner, this also enables an easy way to provide alternative main packages that include or exclude a certain subprogram. For example, profiling support requires importing a lot of additional packages from the standard library, which increases the binary size. As a result, the profiling subprogram is not included in the default main package [src.elv.sh/cmd/elvish], but it is included in the alternative main package [src.elv.sh/cmd/withpprof/elvish]. Binaries built from the former main package is meaningfully smaller than the latter. # Elvish-specific flag handling As general as the [Program] abstraction is, this package has a bit of Elvish-specific flag handling code: - [Run] handles some global flags not specific to any subprogram, like -log. - [FlagSet] handles some flags shared by multiple subprograms, like -json. It's possible to split such code in this package, but doing so seems to require a bit too much indirection to justify for the Elvish codebase. */ package prog import ( "flag" "fmt" "io" "os" "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 = 21 // Program represents a subprogram. // // This is the main abstraction provided by this package. See the package-level // godoc for details. type Program interface { RegisterFlags(fs *FlagSet) // Run runs the subprogram. Run(fds [3]*os.File, args []string) error } func usage(out io.Writer, fs *flag.FlagSet) { fmt.Fprintln(out, "Usage: elvish [flags] [script] [args]") fmt.Fprintln(out, "Supported flags:") fs.SetOutput(out) fs.PrintDefaults() } // Run parses command-line flags and runs the [Program], returning the exit // status. It also handles global flags that are not specific to any subprogram. // // It is supposed to be used from main functions like this: // // func main() { // program := ... // os.Exit(prog.Run([3]*os.File{os.Stdin, os.Stdout, os.Stderr}, os.Args, program)) // } func Run(fds [3]*os.File, args []string, p Program) int { fs := flag.NewFlagSet("elvish", flag.ContinueOnError) // Error and usage will be printed explicitly. fs.SetOutput(io.Discard) var log string var help bool fs.StringVar(&log, "log", "", "Path to a file to write debug logs") fs.BoolVar(&help, "help", false, "Show usage help and quit") fs.IntVar(&DeprecationLevel, "deprecation-level", DeprecationLevel, "Show warnings for all features deprecated as of version 0.X") p.RegisterFlags(&FlagSet{FlagSet: fs}) 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 } if log != "" { err = logutil.SetOutputFile(log) if err == nil { defer logutil.SetOutput(io.Discard) } else { fmt.Fprintln(fds[2], err) } } if help { usage(fds[1], fs) return 0 } err = p.Run(fds, 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] made up from subprograms. It starts from the // first, continuing to the next as long as the subprogram returns an error // created with [NextProgram]. func Composite(programs ...Program) Program { return composite(programs) } type composite []Program func (cp composite) RegisterFlags(f *FlagSet) { for _, p := range cp { p.RegisterFlags(f) } } func (cp composite) Run(fds [3]*os.File, args []string) error { var cleanups []func([3]*os.File) for _, p := range cp { err := p.Run(fds, args) if np, ok := err.(nextProgramError); ok { cleanups = append(cleanups, np.cleanups...) } else { for i := len(cleanups) - 1; i >= 0; i-- { cleanups[i](fds) } return err } } // If we have reached here, all subprograms have returned ErrNextProgram return NextProgram(cleanups...) } // NextProgram returns a special error that may be returned by the Run method of // a [Program] that is part of a [Composite] program, indicating that the next // program should be tried. It can carry a list of cleanup functions that should // be run in reverse order before the composite program finishes. func NextProgram(cleanups ...func([3]*os.File)) error { return nextProgramError{cleanups} } type nextProgramError struct{ cleanups []func([3]*os.File) } // If this error ever gets printed, it has been bubbled to [Run] when all // programs have returned this error type. func (e nextProgramError) Error() string { return "internal error: no suitable subprogram" } // BadUsage returns a special error that may be returned by a [Program]'s Run // method. 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 a [Program]'s Run // method. 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 "" } elvish-0.21.0/pkg/prog/prog_test.elvts000066400000000000000000000060531465720375400177220ustar00rootroot00000000000000//each:program-makers-in-global //each:eval fn p {|opt| program-to-fn (call $make-program~ [] $opt) } //each:eval fn cp {|@opts| program-to-fn (composite (each {|opt| call $make-program~ [] $opt} $opts)) } ///////////////// # flag handling # ///////////////// ## bad flags writes error and usage to stderr ## ~> (p [&]) -bad-flag &check-stderr-contains="flag provided but not defined: -bad-flag\nUsage:" [stderr contains "flag provided but not defined: -bad-flag\nUsage:"] true [exit] 2 // -h is a bad flag too ~> (p [&]) -h &check-stderr-contains="flag provided but not defined: -h\nUsage:" [stderr contains "flag provided but not defined: -h\nUsage:"] true [exit] 2 ## -help writes usage to stdout ## // -help writes usage to stdout instead of stderr ~> (p [&]) -help &check-stdout-contains='Usage: elvish [flags] [script] [args]' [stdout contains "Usage: elvish [flags] [script] [args]"] true ## -log ## //in-temp-dir ~> (p [&]) -log log ~> use os os:exists log ▶ $true ~> (p [&]) -log bad/log &check-stderr-contains='open bad/log:' [stderr contains "open bad/log:"] true ## -deprecation-level ## //deprecation-level 0 //show-deprecation-level-in-global ~> (p [&]) -deprecation-level 42 ~> show-deprecation-level ▶ (num 42) ## custom flag ## ~> (p [&custom-flag]) -flag foo -flag foo ## shared flags ## ~> (p [&shared-flags]) -sock sock -db db -json -sock sock -db db -json true ## multiple subprograms with shared flags ## ~> (cp [&shared-flags &return-err=(next-program)] [&shared-flags]) -sock sock -db db -json -sock sock -db db -json true ////////////////////// # composite programs # ////////////////////// ## runs first subprogram that doesn't return NextProgram ## ~> (cp [&write-stdout="program 1\n"] [&write-stdout="program 2\n"]) program 1 ## NextProgram error skips a program ## ~> (cp [&return-err=(next-program)] [&write-stdout="program 2\n"]) program 2 ## all subprograms return NextProgram prints internal error ## ~> (cp [&return-err=(next-program)] [&return-err=(next-program)]) [stderr] internal error: no suitable subprogram [exit] 2 ## runs cleanup if any subsequent program is run ## ~> (cp [&return-err=(next-program "program 1 cleanup\n")] ^ [&return-err=(next-program "program 2 cleanup\n")] ^ [&write-stdout="program 3\n"]) program 3 program 2 cleanup program 1 cleanup ## runs cleanup if any subsequent program returns non-NextProgram error ## ~> (cp [&return-err=(next-program "program 1 cleanup\n")] ^ [&return-err="program 2 error"]) program 1 cleanup [stderr] program 2 error [exit] 2 ## doesn't run cleanup if all programs return NextProgram error ## ~> (cp [&return-err=(next-program "program 1 cleanup\n")] ^ [&return-err=(next-program)]) [stderr] internal error: no suitable subprogram [exit] 2 ////////////////// # special errors # ////////////////// ## BadUsage ## ~> (p [&return-err=(bad-usage 'lorem ipsum')]) &check-stderr-contains="lorem ipsum\nUsage:" [stderr contains "lorem ipsum\nUsage:"] true [exit] 2 ## Exit ## ~> (p [&return-err=(exit-error 3)]) [exit] 3 ## Exit with 0 ## ~> (p [&return-err=(exit-error 0)]) elvish-0.21.0/pkg/prog/progtest/000077500000000000000000000000001465720375400165005ustar00rootroot00000000000000elvish-0.21.0/pkg/prog/progtest/progtest.go000066400000000000000000000055141465720375400207030ustar00rootroot00000000000000// Package progtest contains utilities for wrapping [prog.Program] instances // into Elvish functions, so that they can be tested using the // [src.elv.sh/pkg/eval/evaltest] framework. package progtest import ( "fmt" "io" "os" "strings" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/must" "src.elv.sh/pkg/prog" ) // ElvishInGlobal returns a setup function suitable for the evaltest framework, // which creates a function called "elvish" in the global scope that invokes the // given program. func ElvishInGlobal(p prog.Program) func(ev *eval.Evaler) { return func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn("elvish", ProgramAsGoFn(p))) } } type programOpts struct { CheckStdoutContains string CheckStderrContains string } func (programOpts) SetDefaultOptions() {} // ProgramAsGoFn converts a [prog.Program] to a Go-implemented Elvish function. // // Stdin of the program is connected to the stdin of the function. // // Stdout of the program is usually written unchanged to the stdout of the // function, except when: // // - If the output has no trailing newline, " (no EOL)\n" is appended. // - If &check-stdout-contains is supplied, stdout is suppressed. Instead, a // tag "[stdout contains foo]" is shown, followed by "true" or "false". // // Stderr of the program is written to the stderr of the function with a // [stderr] prefix, with similar treatment for missing trailing EOL and // &check-stderr-contains. // // If the program exits with a non-zero return value, a line "[exit] $i" is // written to stderr. func ProgramAsGoFn(p prog.Program) any { return func(fm *eval.Frame, opts programOpts, args ...string) { r1, w1 := must.OK2(os.Pipe()) r2, w2 := must.OK2(os.Pipe()) args = append([]string{"elvish"}, args...) exit := prog.Run([3]*os.File{fm.InputFile(), w1, w2}, args, p) w1.Close() w2.Close() outFile := fm.ByteOutput() stdout := string(must.OK1(io.ReadAll(r1))) if s := opts.CheckStdoutContains; s != "" { fmt.Fprintf(outFile, "[stdout contains %q] %t\n", s, strings.Contains(stdout, s)) } else { outFile.WriteString(lines("", stdout)) } errFile := fm.ErrorFile() stderr := string(must.OK1(io.ReadAll(r2))) if s := opts.CheckStderrContains; s != "" { fmt.Fprintf(errFile, "[stderr contains %q] %t\n", s, strings.Contains(stderr, s)) } else { errFile.WriteString(lines("[stderr] ", stderr)) } if exit != 0 { fmt.Fprintf(errFile, "[exit] %d\n", exit) } } } // Splits data into lines, adding prefix to each line and appending " (no EOL)" // if data doesn't end in a newline. func lines(prefix, data string) string { var sb strings.Builder for len(data) > 0 { sb.WriteString(prefix) i := strings.IndexByte(data, '\n') if i == -1 { sb.WriteString(data + " (no EOL)\n") break } else { sb.WriteString(data[:i+1]) data = data[i+1:] } } return sb.String() } elvish-0.21.0/pkg/prog/transcripts_test.go000066400000000000000000000051261465720375400205770ustar00rootroot00000000000000package prog_test import ( "embed" "errors" "fmt" "os" "testing" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/prog/progtest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "show-deprecation-level-in-global", func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn( "show-deprecation-level", func() int { return prog.DeprecationLevel })) }, "program-makers-in-global", func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs(). AddGoFn("program-to-fn", programToFn). AddGoFn("composite", prog.Composite). AddGoFn("make-program", makeProgram). // Error constructors AddGoFn("next-program", nextProgram). AddGoFn("exit-error", func(i int) any { return prog.Exit(i) }). AddGoFn("bad-usage", func(s string) any { return prog.BadUsage(s) })) }) } func programToFn(p prog.Program) eval.Callable { return eval.NewGoFn("test-program", progtest.ProgramAsGoFn(p)) } type makeProgramOpts struct { WriteStdout string CustomFlag bool SharedFlags bool ReturnErr any } func (makeProgramOpts) SetDefaultOptions() {} func makeProgram(opts makeProgramOpts) prog.Program { var returnErr error switch e := opts.ReturnErr.(type) { case error: returnErr = e case string: returnErr = errors.New(e) case nil: // Do nothing default: panic("&return-err should be error or string") } return &testProgram{ writeStdout: opts.WriteStdout, customFlag: opts.CustomFlag, sharedFlags: opts.SharedFlags, returnErr: returnErr, } } func nextProgram(cleanupPrint ...string) any { switch len(cleanupPrint) { case 0: return prog.NextProgram() case 1: return prog.NextProgram(func(fds [3]*os.File) { fds[1].WriteString(cleanupPrint[0]) }) default: panic("next-program takes 0 or 1 argument") } } type testProgram struct { writeStdout string customFlag bool sharedFlags bool returnErr error flag string daemonPaths *prog.DaemonPaths json *bool } func (p *testProgram) RegisterFlags(f *prog.FlagSet) { if p.customFlag { f.StringVar(&p.flag, "flag", "default", "a flag") } if p.sharedFlags { p.daemonPaths = f.DaemonPaths() p.json = f.JSON() } } func (p *testProgram) Run(fds [3]*os.File, args []string) error { if p.returnErr != nil { return p.returnErr } fds[1].WriteString(p.writeStdout) if p.customFlag { fmt.Fprintf(fds[1], "-flag %s\n", p.flag) } if p.sharedFlags { fmt.Fprintf(fds[1], "-sock %s -db %s -json %v\n", p.daemonPaths.Sock, p.daemonPaths.DB, *p.json) } return nil } elvish-0.21.0/pkg/rpc/000077500000000000000000000000001465720375400144465ustar00rootroot00000000000000elvish-0.21.0/pkg/rpc/LICENSE000066400000000000000000000027071465720375400154610ustar00rootroot00000000000000Copyright (c) 2009 The Go Authors. 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. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 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 OWNER 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.21.0/pkg/rpc/client.go000066400000000000000000000176751465720375400162730ustar00rootroot00000000000000// 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 any // The argument to the function (*struct). Reply any // 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, any) error ReadResponseHeader(*Response) error ReadResponseBody(any) 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 any) (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 any) 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 any, reply any, 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 any, reply any) error { call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done return call.Error } elvish-0.21.0/pkg/rpc/debug.go000066400000000000000000000006041465720375400160630ustar00rootroot00000000000000// 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.21.0/pkg/rpc/server.go000066400000000000000000000501271465720375400163100ustar00rootroot00000000000000// 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 any) 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 any) error { return server.register(rcvr, name, true) } func (server *Server) register(rcvr any, 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 any, 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 any) error { return c.dec.Decode(body) } func (c *gobServerCodec) WriteResponse(r *Response, body any) (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 any) 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 any) 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(any) error WriteResponse(*Response, any) 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.21.0/pkg/shell/000077500000000000000000000000001465720375400147715ustar00rootroot00000000000000elvish-0.21.0/pkg/shell/interact.go000066400000000000000000000111261465720375400171320ustar00rootroot00000000000000package 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" "src.elv.sh/pkg/ui" ) // 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() } var daemonClient daemondefs.Client 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.") } if cl != nil { // Even if error is not nil, we install daemon-related // functionalities anyway. Daemon may eventually come online and // become functional. daemonClient = cl ev.PreExitHooks = append(ev.PreExitHooks, 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].Fd()) { restoreTTY := term.SetupForTUIOnce(fds[0], fds[1]) defer restoreTTY() newed := edit.NewEditor(cli.NewTTY(fds[0], fds[2]), ev, daemonClient) ev.ExtendBuiltin(eval.BuildNs().AddNs("edit", newed)) ev.BgJobNotify = func(s string) { newed.Notify(ui.T(s)) } 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) } } 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 } err = evalInTTY(fds, ev, ed, parse.Source{Name: fmt.Sprintf("[tty %v]", cmdNum), Code: line}) 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 } return evalInTTY(fds, ev, ed, parse.Source{Name: absPath, Code: code, IsFile: true}) } 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.21.0/pkg/shell/interact_test.elvts000066400000000000000000000046361465720375400207310ustar00rootroot00000000000000//unset-env XDG_CONFIG_HOME //unset-env XDG_DATA_HOME //each:in-temp-home //each:elvish-in-global //each:eval use os ///////////////// # Evaluate code # ///////////////// ~> echo 'echo hello' | elvish 2>$os:dev-null hello /////////////////// # Print exception # /////////////////// ~> echo 'fail error' | elvish &check-stderr-contains='fail error' [stderr contains "fail error"] true //////////////////// # Evaluate rc file # //////////////////// ~> echo 'echo hello from rc.elv' > rc.elv ~> echo | elvish -rc rc.elv 2>$os:dev-null hello from rc.elv ## rc file doesn't compile ## ~> echo 'echo $a' > rc.elv ~> echo | elvish -rc rc.elv &check-stderr-contains='variable $a not found' [stderr contains "variable $a not found"] true ## rc file throws exception ## ~> echo 'fail bad' > rc.elv ~> echo | elvish -rc rc.elv &check-stderr-contains='fail bad' [stderr contains "fail bad"] true ## rc file not existing is OK ## ~> echo | elvish -rc nonexistent.elv 2>$os:dev-null //////////////// # Find RC file # //////////////// ## ~/.config/elvish on Unix ## //only-on unix ~> os:mkdir-all .config/elvish ~> echo 'echo hello home config' > .config/elvish/rc.elv ~> echo | elvish 2>$os:dev-null hello home config ## XDG_CONFIG_HOME on all platforms ## ~> os:mkdir-all xdg_config_home/elvish ~> echo 'echo hello XDG_CONFIG_HOME' > xdg_config_home/elvish/rc.elv ~> set E:XDG_CONFIG_HOME = ~/xdg_config_home ~> echo | elvish 2>$os:dev-null hello XDG_CONFIG_HOME /////////////////// # Daemon behavior # /////////////////// //each:elvish-with-activate-daemon-in-global //each:in-temp-home //each:unset-env XDG_STATE_HOME ## establish connection ## ~> == $pid (echo 'use daemon; echo $daemon:pid' | elvish 2>$os:dev-null) ▶ $true ## does not store empty command in history ## ~> echo "\nuse store; store:next-cmd-seq" | elvish 2>$os:dev-null ▶ (num 1) ## stores DB under ~/.local/state/elvish by default on Unix ## //only-on unix ~> echo "" | elvish 2>$os:dev-null ~> os:exists ~/.local/state/elvish/db.bolt ▶ $true ## respects XDG_STATE_HOME for DB path ## //in-temp-dir ~> use os os:mkdir xdg-state-home set E:XDG_STATE_HOME = $pwd/xdg-state-home ~> echo "" | elvish 2>$os:dev-null ~> os:exists xdg-state-home/elvish/db.bolt ▶ $true ## connection failure ## //elvish-with-bad-activate-daemon-in-global ~> echo | elvish &check-stderr-contains='Cannot connect to daemon: fake error' [stderr contains "Cannot connect to daemon: fake error"] true elvish-0.21.0/pkg/shell/paths.go000066400000000000000000000050711465720375400164420ustar00rootroot00000000000000package shell import ( "fmt" "os" "path/filepath" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/env" "src.elv.sh/pkg/prog" ) func rcPath() (string, error) { if configHome := os.Getenv(env.XDG_CONFIG_HOME); configHome != "" { return filepath.Join(configHome, "elvish", "rc.elv"), nil } else if configHome, err := defaultConfigHome(); err == nil { return filepath.Join(configHome, "elvish", "rc.elv"), nil } else { return "", fmt.Errorf("find rc.elv: %w", err) } } func libPaths() ([]string, error) { var paths []string if configHome := os.Getenv(env.XDG_CONFIG_HOME); configHome != "" { paths = append(paths, filepath.Join(configHome, "elvish", "lib")) } else if configHome, err := defaultConfigHome(); err == nil { paths = append(paths, filepath.Join(configHome, "elvish", "lib")) } else { return nil, fmt.Errorf("find roaming lib directory: %w", err) } if dataHome := os.Getenv(env.XDG_DATA_HOME); dataHome != "" { paths = append(paths, filepath.Join(dataHome, "elvish", "lib")) } else if dataHome, err := defaultDataHome(); err == nil { paths = append(paths, filepath.Join(dataHome, "elvish", "lib")) } else { return nil, fmt.Errorf("find local lib directory: %w", err) } if dataDirs := os.Getenv(env.XDG_DATA_DIRS); dataDirs != "" { // XDG requires the paths be joined with ":". However, on Windows ":" // appear after the drive letter, so it's infeasible to use it to also // join paths. for _, dataDir := range filepath.SplitList(dataDirs) { paths = append(paths, filepath.Join(dataDir, "elvish", "lib")) } } else { paths = append(paths, defaultDataDirs...) } return paths, nil } // Returns a SpawnConfig containing all the paths needed by the daemon. It // respects overrides of sock and db from CLI flags. func daemonPaths(p *prog.DaemonPaths) (*daemondefs.SpawnConfig, error) { runDir, err := secureRunDir() if err != nil { return nil, err } sock := p.Sock if sock == "" { sock = filepath.Join(runDir, "sock") } db := p.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 stateHome := os.Getenv(env.XDG_STATE_HOME); stateHome != "" { return filepath.Join(stateHome, "elvish", "db.bolt"), nil } else if stateHome, err := defaultStateHome(); err == nil { return filepath.Join(stateHome, "elvish", "db.bolt"), nil } else { return "", fmt.Errorf("find db: %w", err) } } elvish-0.21.0/pkg/shell/paths_unix.go000066400000000000000000000040631465720375400175050ustar00rootroot00000000000000//go:build unix package shell import ( "fmt" "os" "path/filepath" "syscall" "src.elv.sh/pkg/env" "src.elv.sh/pkg/fsutil" ) func defaultConfigHome() (string, error) { return homePath(".config") } func defaultDataHome() (string, error) { return homePath(".local/share") } var defaultDataDirs = []string{ "/usr/local/share/elvish/lib", "/usr/share/elvish/lib", } func defaultStateHome() (string, error) { return homePath(".local/state") } func homePath(suffix string) (string, error) { home, err := fsutil.GetHome("") if err != nil { return "", fmt.Errorf("resolve ~/%s: %w", suffix, err) } return filepath.Join(home, 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.21.0/pkg/shell/paths_unix_test.go000066400000000000000000000034201465720375400205400ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/shell/paths_windows.go000066400000000000000000000016141465720375400202130ustar00rootroot00000000000000package shell import ( "fmt" "os" "path/filepath" "golang.org/x/sys/windows" "src.elv.sh/pkg/env" ) var ( defaultConfigHome = roamingAppData defaultDataHome = localAppData defaultDataDirs = []string{} defaultStateHome = localAppData ) 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.21.0/pkg/shell/script.go000066400000000000000000000050141465720375400166240ustar00rootroot00000000000000package 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/eval/vals" "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.Args = vals.MakeListSlice(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(fds, ev, nil, 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, compileErr error) []byte { var converted []errorInJSON for _, e := range parse.UnpackErrors(parseErr) { converted = append(converted, errorInJSON{e.Context.Name, e.Context.From, e.Context.To, e.Message}) } for _, e := range eval.UnpackCompilationErrors(compileErr) { converted = append(converted, errorInJSON{e.Context.Name, e.Context.From, e.Context.To, e.Message}) } jsonError, errMarshal := json.Marshal(converted) if errMarshal != nil { return []byte(`[{"message":"Unable to convert the errors to JSON"}]`) } return jsonError } elvish-0.21.0/pkg/shell/script_test.elvts000066400000000000000000000047531465720375400204240ustar00rootroot00000000000000//each:elvish-in-global //////////// # Run file # //////////// //in-temp-dir ~> echo 'echo hello' > hello.elv ~> elvish hello.elv hello ## Invalid UTF-8 ## //in-temp-dir ~> echo "\xff" > invalid-utf8.elv ~> elvish invalid-utf8.elv &check-stderr-contains='cannot read script' [stderr contains "cannot read script"] true [exit] 2 ## Non-existing file ## //in-temp-dir ~> elvish non-existing.elv &check-stderr-contains='cannot read script' [stderr contains "cannot read script"] true [exit] 2 //////////////////// # Run code with -c # //////////////////// ~> elvish -c 'echo hello' hello // TODO: -c should also reject source with invalid UTF-8 // For simplicity, the remaining tests use -c wherever to avoid the need to set // up temporary files. /////////////// # Parse error # /////////////// ~> elvish -c 'echo [' [stderr] Parse error: should be ']' [stderr] code from -c:1:7: echo [ [exit] 2 ## Parse errors are shown with -compileonly ## ~> elvish -compileonly -c 'echo [' [stderr] Parse error: should be ']' [stderr] code from -c:1:7: echo [ [exit] 2 ## Parse errors with -compileonly and -json ## ~> elvish -compileonly -json -c 'echo [' [{"fileName":"code from -c","start":6,"end":6,"message":"should be ']'"}] [exit] 2 ## Multiple parse errors with -compileonly and -json ## ~> elvish -compileonly -json -c 'echo [{' [{"fileName":"code from -c","start":7,"end":7,"message":"should be ',' or '}'"},{"fileName":"code from -c","start":7,"end":7,"message":"should be ']'"}] [exit] 2 ///////////////////// # Compilation error # ///////////////////// ~> elvish -c "echo $a" [stderr] Compilation error: variable $a not found [stderr] code from -c:1:6-7: echo $a [exit] 2 ## With -compileonly ## ~> elvish -compileonly -c "echo $a" [stderr] Compilation error: variable $a not found [stderr] code from -c:1:6-7: echo $a [exit] 2 ## With -compileonly and -json ## ~> elvish -compileonly -json -c "echo $a" [{"fileName":"code from -c","start":5,"end":7,"message":"variable $a not found"}] [exit] 2 ## Both parse error and compilation error With -compileonly and -json ## ~> elvish -compileonly -json -c "echo [$a" [{"fileName":"code from -c","start":8,"end":8,"message":"should be ']'"},{"fileName":"code from -c","start":6,"end":8,"message":"variable $a not found"}] [exit] 2 ///////////// # Exception # ///////////// ~> elvish -c 'fail failure' [stderr] Exception: failure [stderr] code from -c:1:1-12: fail failure [exit] 2 ## Doesn't get triggered with -compileonly ## ~> elvish -compileonly -c 'fail failure' elvish-0.21.0/pkg/shell/shell.go000066400000000000000000000121171465720375400164310ustar00rootroot00000000000000// Package shell is the entry point for the terminal interface of Elvish. package shell import ( "fmt" "io" "os" "os/signal" "path/filepath" "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/parse" "src.elv.sh/pkg/prog" "src.elv.sh/pkg/sys" "src.elv.sh/pkg/ui" ) var logger = logutil.GetLogger("[shell] ") // Program is the shell subprogram. It has two slightly different modes of // operation: // // - When the command line argument contains a filename or "-c some-code", the // shell is non-interactive. In this mode, it just evaluates the given file or // code. // // - Otherwise, the shell is interactive, and launches a terminal [REPL]. This // mode also initializes the storage backend, which in turn activates the // storage daemon. // // To enable building a daemon-less version, the subprogram doesn't depend // on pkg/daemon, and the caller should supply pkg/daemon.Activate in the // ActivateDaemon field to enable functionalities. If it is nil, daemon // functionalities are disabled. // // [REPL]: https://en.wikipedia.org/wiki/Read–eval–print_loop type Program struct { ActivateDaemon daemondefs.ActivateFunc codeInArg bool compileOnly bool noRC bool rc string json *bool daemonPaths *prog.DaemonPaths } func (p *Program) RegisterFlags(fs *prog.FlagSet) { // script(1) (and possibly other programs) assume shells support -i fs.Bool("i", false, "A no-op flag, introduced for compatibility") // termux (and possibly other programs) assume shells support -l fs.Bool("l", false, "A no-op flag, introduced for compatibility") fs.BoolVar(&p.codeInArg, "c", false, "Treat the first argument as code to execute") fs.BoolVar(&p.compileOnly, "compileonly", false, "Parse and compile Elvish code without executing it") fs.BoolVar(&p.noRC, "norc", false, "Don't read the RC file when running interactively") fs.StringVar(&p.rc, "rc", "", "Path to the RC file when running interactively") p.json = fs.JSON() if p.ActivateDaemon != nil { p.daemonPaths = fs.DaemonPaths() } } func (p *Program) Run(fds [3]*os.File, args []string) error { cleanup1 := incSHLVL() defer cleanup1() cleanup2 := initSignal(fds) defer cleanup2() // https://no-color.org ui.NoColor = os.Getenv(env.NO_COLOR) != "" interactive := len(args) == 0 ev := p.makeEvaler(fds[2], interactive) defer ev.PreExit() if !interactive { exit := script( ev, fds, args, &scriptCfg{ Cmd: p.codeInArg, CompileOnly: p.compileOnly, JSON: *p.json}) return prog.Exit(exit) } var spawnCfg *daemondefs.SpawnConfig if p.ActivateDaemon != nil { var err error spawnCfg, err = daemonPaths(p.daemonPaths) if err != nil { fmt.Fprintln(fds[2], "Warning:", err) fmt.Fprintln(fds[2], "Storage daemon may not function.") } } interact(ev, fds, &interactCfg{ RC: ev.EffectiveRcPath, ActivateDaemon: p.ActivateDaemon, SpawnConfig: spawnCfg}) return nil } // 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 (p *Program) makeEvaler(stderr io.Writer, interactive bool) *eval.Evaler { ev := eval.NewEvaler() var errRc error ev.RcPath, errRc = rcPath() switch { case !interactive || p.noRC: // Leave ev.ActualRcPath empty case p.rc != "": // Use explicit -rc flag value var err error ev.EffectiveRcPath, err = filepath.Abs(p.rc) if err != nil { fmt.Fprintln(stderr, "Warning:", err) } default: if errRc == nil { // Use default path stored in ev.RcPath ev.EffectiveRcPath = ev.RcPath } else { fmt.Fprintln(stderr, "Warning:", errRc) } } libs, err := libPaths() if err != nil { fmt.Fprintln(stderr, "Warning: resolving lib paths:", err) } else { ev.LibDirs = libs } mods.AddTo(ev) return ev } // 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 initSignal(fds [3]*os.File) func() { sigCh := sys.NotifySignals() go func() { for sig := range sigCh { logger.Println("signal", sig) handleSignal(sig, fds[2]) } }() return func() { signal.Stop(sigCh) close(sigCh) } } func evalInTTY(fds [3]*os.File, ev *eval.Evaler, ed editor, src parse.Source) error { start := time.Now() ports, cleanup := eval.PortsFromFiles(fds, ev.ValuePrefix()) defer cleanup() restore := term.SetupForEval(fds[0], fds[1]) defer restore() ctx, done := eval.ListenInterrupts() err := ev.Eval(src, eval.EvalCfg{ Ports: ports, Interrupts: ctx, PutInFg: true}) done() if ed != nil { ed.RunAfterCommandHooks(src, time.Since(start).Seconds(), err) } return err } elvish-0.21.0/pkg/shell/shell_test.elvts000066400000000000000000000057161465720375400202270ustar00rootroot00000000000000//each:elvish-in-global /////////////// # no-op flags # /////////////// ~> elvish -i -l -c 'echo hello' hello ///////////////////// # XDG library paths # ///////////////////// //in-temp-dir //unset-env XDG_CONFIG_HOME //unset-env XDG_DATA_HOME //unset-env XDG_DATA_DIRS ~> use os use str use path ~> fn make-lib {|root mods| os:mkdir-all $root/elvish/lib for mod $mods { echo 'echo '$mod' from '$root > $root/elvish/lib/$mod.elv } } // Setting up XDG library paths, from highest priority to lowest ~> make-lib xdg-config-home [a] set E:XDG_CONFIG_HOME = $pwd/xdg-config-home ~> make-lib xdg-data-home [a b] set E:XDG_DATA_HOME = $pwd/xdg-data-home ~> make-lib xdg-data-dir-1 [a b c] make-lib xdg-data-dir-2 [a b c d] set E:XDG_DATA_DIRS = (str:join $path:list-separator [$pwd/xdg-data-dir-{1 2}]) ~> elvish -c 'use a' a from xdg-config-home ~> elvish -c 'use b' b from xdg-data-home ~> elvish -c 'use c' c from xdg-data-dir-1 ~> elvish -c 'use d' d from xdg-data-dir-2 //////////////////////// # Support for NO_COLOR # //////////////////////// //each:unset-env NO_COLOR ## unset: color works ## ~> elvish -c 'to-string (styled foo red)' ▶ "\e[;31mfoo\e[m" ## empty: color works ## ~> set E:NO_COLOR = '' ~> elvish -c 'to-string (styled foo red)' ▶ "\e[;31mfoo\e[m" ## non-empty: color suppressed ## ~> set E:NO_COLOR = yes ~> elvish -c 'to-string (styled foo red)' ▶ "\e[mfoo" ~> set E:NO_COLOR = 1 ~> elvish -c 'to-string (styled foo red)' ▶ "\e[mfoo" // https://no-color.org specifies that *any* non-empty value suppresses color, // regardless of the value. ~> set E:NO_COLOR = no ~> elvish -c 'to-string (styled foo red)' ▶ "\e[mfoo" ///////// # SHLVL # ///////// //each:unset-env SHLVL ## non-negative value gets incremented ## ~> set E:SHLVL = 0 ~> elvish -c 'echo $E:SHLVL' 1 ~> echo $E:SHLVL 0 ~> set E:SHLVL = 10 ~> elvish -c 'echo $E:SHLVL' 11 ~> echo $E:SHLVL 10 ## unset is treated like 0 ## ~> elvish -c 'echo $E:SHLVL' 1 ~> has-env SHLVL ▶ $false ## invalid value is treated like 0 ## ~> set E:SHLVL = invalid ~> elvish -c 'echo $E:SHLVL' 1 ~> echo $E:SHLVL invalid ## negative value gets incremented ## // Other shells don't agree on what to do when SHLVL is negative: // // ~> env SHLVL=-100 bash -c 'echo $SHLVL' // 0 // ~> env SHLVL=-100 zsh -c 'echo $SHLVL' // -99 // ~> env SHLVL=-100 fish -c 'echo $SHLVL' // 1 // // Elvish follows Zsh here. ~> set E:SHLVL = -100 ~> elvish -c 'echo $E:SHLVL' -99 ~> echo $E:SHLVL -100 /////////////////////////// # signal handling on Unix # /////////////////////////// //only-on unix ## dump stack trace on USR1 ## //kill-wait-in-global ~> elvish -c 'kill -USR1 $pid; sleep '$kill-wait &check-stderr-contains='src.elv.sh/pkg/shell' [stderr contains "src.elv.sh/pkg/shell"] true ## ignore but log CHLD ## //in-temp-dir //kill-wait-in-global //sigchld-name-in-global ~> elvish -log log -c 'kill -CHLD $pid; sleep '$kill-wait ~> use str ~> str:contains (slurp < log) 'signal '$sigchld-name ▶ $true elvish-0.21.0/pkg/shell/signal_unix.go000066400000000000000000000004531465720375400176420ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/shell/signal_windows.go000066400000000000000000000003741465720375400203530ustar00rootroot00000000000000package shell import ( "io" "os" "syscall" ) func handleSignal(sig os.Signal, stderr io.Writer) { switch sig { // See https://pkg.go.dev/os/signal#hdr-Windows for the semantics of SIGTERM // on Windows. case syscall.SIGTERM: os.Exit(0) } } elvish-0.21.0/pkg/shell/transcripts_test.go000066400000000000000000000050621465720375400207360ustar00rootroot00000000000000package shell_test import ( "embed" "errors" "io" "os" "path/filepath" "testing" "time" "src.elv.sh/pkg/daemon" "src.elv.sh/pkg/daemon/daemondefs" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" "src.elv.sh/pkg/eval/vars" "src.elv.sh/pkg/prog/progtest" "src.elv.sh/pkg/shell" "src.elv.sh/pkg/testutil" ) //go:embed *.elvts var transcripts embed.FS var sigCHLDName = "" func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvish-in-global", progtest.ElvishInGlobal(&shell.Program{}), "elvish-with-activate-daemon-in-global", progtest.ElvishInGlobal( &shell.Program{ActivateDaemon: inProcessActivateFunc(t)}), "elvish-with-bad-activate-daemon-in-global", progtest.ElvishInGlobal( &shell.Program{ ActivateDaemon: func(io.Writer, *daemondefs.SpawnConfig) (daemondefs.Client, error) { return nil, errors.New("fake error") }, }), "kill-wait-in-global", addGlobal("kill-wait", testutil.Scaled(10*time.Millisecond).String()), "sigchld-name-in-global", addGlobal("sigchld-name", sigCHLDName), "in-temp-home", func(t *testing.T) { testutil.InTempHome(t) }, ) } func inProcessActivateFunc(t *testing.T) daemondefs.ActivateFunc { return func(stderr io.Writer, cfg *daemondefs.SpawnConfig) (daemondefs.Client, error) { // Start an in-process daemon. // // Create the socket in a temporary directory. This is necessary because // we don't do enough mocking in the tests yet, and cfg.SockPath will // point to the socket used by real Elvish sessions. dir := testutil.TempDir(t) sockPath := filepath.Join(dir, "sock") sigCh := make(chan os.Signal) readyCh := make(chan struct{}) daemonDone := make(chan struct{}) go func() { // Unlike the socket path, we do honor cfg.DBPath; this is because // we run tests in a temporary HOME, so there's no risk of using the // DB of real Elvish sessions. daemon.Serve(sockPath, cfg.DbPath, daemon.ServeOpts{Ready: readyCh, Signals: sigCh}) close(daemonDone) }() t.Cleanup(func() { close(sigCh) select { case <-daemonDone: case <-time.After(testutil.Scaled(2 * time.Second)): t.Errorf("timed out waiting for daemon to quit") } }) select { case <-readyCh: // Do nothing case <-time.After(testutil.Scaled(2 * time.Second)): t.Fatalf("timed out waiting for daemon to start") } // Connect to it. return daemon.NewClient(sockPath), nil } } func addGlobal(name string, value any) func(ev *eval.Evaler) { return func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddVar(name, vars.NewReadOnly(value))) } } elvish-0.21.0/pkg/shell/transcripts_unix_test.go000066400000000000000000000001571465720375400220010ustar00rootroot00000000000000//go:build unix package shell_test import "syscall" func init() { sigCHLDName = syscall.SIGCHLD.String() } elvish-0.21.0/pkg/store/000077500000000000000000000000001465720375400150165ustar00rootroot00000000000000elvish-0.21.0/pkg/store/buckets.go000066400000000000000000000002151465720375400170030ustar00rootroot00000000000000package store const ( bucketCmd = "cmd" bucketDir = "dir" ) // The following buckets were used before and are thus reserved: // "schema" elvish-0.21.0/pkg/store/cmd.go000066400000000000000000000072171465720375400161170ustar00rootroot00000000000000package 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.21.0/pkg/store/cmd_test.go000066400000000000000000000002631465720375400171500ustar00rootroot00000000000000package 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.21.0/pkg/store/db_store.go000066400000000000000000000035431465720375400171530ustar00rootroot00000000000000package 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.21.0/pkg/store/dir.go000066400000000000000000000046601465720375400161310ustar00rootroot00000000000000package 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.21.0/pkg/store/dir_test.go000066400000000000000000000002631465720375400171630ustar00rootroot00000000000000package 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.21.0/pkg/store/staticcheck.conf000066400000000000000000000000721465720375400201510ustar00rootroot00000000000000dot_import_whitelist = ["src.elv.sh/pkg/store/storedefs"] elvish-0.21.0/pkg/store/store.go000066400000000000000000000001061465720375400164760ustar00rootroot00000000000000// Package store defines the permanent storage service. package store elvish-0.21.0/pkg/store/storedefs/000077500000000000000000000000001465720375400170145ustar00rootroot00000000000000elvish-0.21.0/pkg/store/storedefs/storedefs.go000066400000000000000000000022661465720375400213470ustar00rootroot00000000000000// 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) } // 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.21.0/pkg/store/storetest/000077500000000000000000000000001465720375400170525ustar00rootroot00000000000000elvish-0.21.0/pkg/store/storetest/cmd.go000066400000000000000000000056311465720375400201510ustar00rootroot00000000000000package 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.21.0/pkg/store/storetest/dir.go000066400000000000000000000025021465720375400201560ustar00rootroot00000000000000package 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.21.0/pkg/store/storetest/storetest.go000066400000000000000000000003211465720375400214310ustar00rootroot00000000000000// 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.21.0/pkg/store/temp_store.go000066400000000000000000000015761465720375400175370ustar00rootroot00000000000000package 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.21.0/pkg/strutil/000077500000000000000000000000001465720375400153705ustar00rootroot00000000000000elvish-0.21.0/pkg/strutil/camel_to_dashed.go000066400000000000000000000011641465720375400210140ustar00rootroot00000000000000package strutil import ( "strings" "unicode" ) // CamelToDashed converts a CamelCaseIdentifier to a dash-separated-identifier, // or a camelCaseIdentifier to a -dash-separated-identifier. All-cap words // are converted to lower case; HTTP becomes http and HTTPRequest becomes // http-request. func CamelToDashed(camel string) string { var sb strings.Builder runes := []rune(camel) for i, r := range runes { if (i == 0 && unicode.IsLower(r)) || (0 < i && i < len(runes)-1 && unicode.IsUpper(r) && unicode.IsLower(runes[i+1])) { sb.WriteRune('-') } sb.WriteRune(unicode.ToLower(r)) } return sb.String() } elvish-0.21.0/pkg/strutil/camel_to_dashed_test.go000066400000000000000000000004631465720375400220540ustar00rootroot00000000000000package strutil import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestCamelToDashed(t *testing.T) { tt.Test(t, CamelToDashed, Args("CamelCase").Rets("camel-case"), Args("camelCase").Rets("-camel-case"), Args("HTTP").Rets("http"), Args("HTTPRequest").Rets("http-request"), ) } elvish-0.21.0/pkg/strutil/chop.go000066400000000000000000000012221465720375400166450ustar00rootroot00000000000000package 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.21.0/pkg/strutil/chop_test.go000066400000000000000000000013051465720375400177060ustar00rootroot00000000000000package strutil import ( "testing" "src.elv.sh/pkg/tt" ) func TestChopLineEnding(t *testing.T) { tt.Test(t, ChopLineEnding, 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) { tt.Test(t, ChopTerminator, 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.21.0/pkg/strutil/eol_sol.go000066400000000000000000000006161465720375400173560ustar00rootroot00000000000000package 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.21.0/pkg/strutil/eol_sol_test.go000066400000000000000000000010371465720375400204130ustar00rootroot00000000000000package 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.21.0/pkg/strutil/join_lines.go000066400000000000000000000003351465720375400200510ustar00rootroot00000000000000package strutil import "strings" // JoinLines appends each line with a "\n" and joins all of them. func JoinLines(lines []string) string { if len(lines) == 0 { return "" } return strings.Join(lines, "\n") + "\n" } elvish-0.21.0/pkg/strutil/join_lines_test.go000066400000000000000000000004621465720375400211110ustar00rootroot00000000000000package strutil_test import ( "testing" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestJoinLines(t *testing.T) { tt.Test(t, strutil.JoinLines, Args([]string(nil)).Rets(""), Args([]string{"foo"}).Rets("foo\n"), Args([]string{"foo", "bar"}).Rets("foo\nbar\n"), ) } elvish-0.21.0/pkg/strutil/strutil.go000066400000000000000000000000761465720375400174300ustar00rootroot00000000000000// Package strutil provides string utilities. package strutil elvish-0.21.0/pkg/strutil/subseq.go000066400000000000000000000006351465720375400172250ustar00rootroot00000000000000package 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.21.0/pkg/strutil/subseq_test.go000066400000000000000000000011571465720375400202640ustar00rootroot00000000000000package 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.21.0/pkg/strutil/title.go000066400000000000000000000004451465720375400170430ustar00rootroot00000000000000package strutil import ( "unicode" "unicode/utf8" ) // Title returns s with the first codepoint changed to title case. func Title(s string) string { r, size := utf8.DecodeRuneInString(s) if r == utf8.RuneError || size == 0 { return s } return string(unicode.ToTitle(r)) + s[size:] } elvish-0.21.0/pkg/strutil/title_test.go000066400000000000000000000003351465720375400201000ustar00rootroot00000000000000package strutil import ( "testing" "src.elv.sh/pkg/tt" ) func TestTitle(t *testing.T) { tt.Test(t, Title, Args("").Rets(""), Args("foo").Rets("Foo"), Args("\xf0").Rets("\xf0"), Args("FOO").Rets("FOO"), ) } elvish-0.21.0/pkg/sys/000077500000000000000000000000001465720375400145005ustar00rootroot00000000000000elvish-0.21.0/pkg/sys/dumpstack.go000066400000000000000000000004061465720375400170220ustar00rootroot00000000000000package sys import "runtime" const dumpStackBufSizeInit = 8192 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.21.0/pkg/sys/eunix/000077500000000000000000000000001465720375400156305ustar00rootroot00000000000000elvish-0.21.0/pkg/sys/eunix/eunix.go000066400000000000000000000001371465720375400173100ustar00rootroot00000000000000//go:build unix // Package eunix provides extra Unix-specific system utilities. package eunix elvish-0.21.0/pkg/sys/eunix/tc.go000066400000000000000000000003411465720375400165630ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/sys/eunix/termios.go000066400000000000000000000034071465720375400176450ustar00rootroot00000000000000//go:build unix // 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) } // SetIXON sets the IXON iflag bit. func (term *Termios) SetIXON(v bool) { setFlag(&term.Iflag, unix.IXON, v) } func setFlag(flag *termiosFlag, mask termiosFlag, v bool) { if v { *flag |= mask } else { *flag &= ^mask } } elvish-0.21.0/pkg/sys/eunix/termios_bsd.go000066400000000000000000000006731465720375400204770ustar00rootroot00000000000000//go: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.21.0/pkg/sys/eunix/termios_notbsd.go000066400000000000000000000006151465720375400212140ustar00rootroot00000000000000//go: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.21.0/pkg/sys/eunix/termiosflag_darwin.go000066400000000000000000000004611465720375400220400ustar00rootroot00000000000000package 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 // // Darwin uses 32-bit flags on 32-bit architectures, but Go no longer supports // them since Go 1.15. type termiosFlag = uint64 elvish-0.21.0/pkg/sys/eunix/termiosflag_notdarwin.go000066400000000000000000000001051465720375400225540ustar00rootroot00000000000000//go:build unix && !darwin package eunix type termiosFlag = uint32 elvish-0.21.0/pkg/sys/eunix/waitforread.go000066400000000000000000000015621465720375400204720ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/sys/eunix/waitforread_test.go000066400000000000000000000007711465720375400215320ustar00rootroot00000000000000//go:build unix package eunix import ( "io" "testing" "src.elv.sh/pkg/must" ) func TestWaitForRead(t *testing.T) { r0, w0 := must.Pipe() r1, w1 := must.Pipe() 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.21.0/pkg/sys/ewindows/000077500000000000000000000000001465720375400163375ustar00rootroot00000000000000elvish-0.21.0/pkg/sys/ewindows/console.go000066400000000000000000000032131465720375400203270ustar00rootroot00000000000000//go: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.21.0/pkg/sys/ewindows/ewindows.go000066400000000000000000000004531465720375400205270ustar00rootroot00000000000000//go:generate cmd /c go tool cgo -godefs types.go > ztypes_windows.go && gofmt -w ztypes_windows.go //go: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.21.0/pkg/sys/ewindows/types.go000066400000000000000000000011161465720375400200310ustar00rootroot00000000000000//go: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.21.0/pkg/sys/ewindows/wait.go000066400000000000000000000025251465720375400176360ustar00rootroot00000000000000//go: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.21.0/pkg/sys/ewindows/ztypes_windows.go000066400000000000000000000015271465720375400220030ustar00rootroot00000000000000// Code generated by cmd/cgo -godefs; DO NOT EDIT. // cgo.exe -godefs 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.21.0/pkg/sys/signal_nonunix.go000066400000000000000000000004211465720375400200570ustar00rootroot00000000000000//go: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.21.0/pkg/sys/signal_unix.go000066400000000000000000000012211465720375400173430ustar00rootroot00000000000000//go:build unix 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.21.0/pkg/sys/sys.go000066400000000000000000000013461465720375400156510ustar00rootroot00000000000000// 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(fd uintptr) bool { return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) } elvish-0.21.0/pkg/sys/winsize_unix.go000066400000000000000000000011611465720375400175610ustar00rootroot00000000000000//go:build unix // Copyright 2015 go-termios Author. All Rights Reserved. // https://github.com/go-termios/termios // Author: John Lenton package sys import ( "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 { 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.21.0/pkg/sys/winsize_windows.go000066400000000000000000000007211465720375400202710ustar00rootroot00000000000000package sys import ( "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 { return -1, -1 } window := info.Window return int(window.Bottom - window.Top), int(window.Right - window.Left) } elvish-0.21.0/pkg/testutil/000077500000000000000000000000001465720375400155375ustar00rootroot00000000000000elvish-0.21.0/pkg/testutil/chmod.go000066400000000000000000000007051465720375400171620ustar00rootroot00000000000000package testutil import ( "io/fs" "os" ) // ChmodOrSkip runs [os.Chmod], but skips the test if file's mode is not exactly // mode or if there is any error. func ChmodOrSkip(s Skipper, name string, mode fs.FileMode) { err := os.Chmod(name, mode) if err != nil { s.Skipf("chmod: %v", err) } fi, err := os.Stat(name) if err != nil { s.Skipf("stat: %v", err) } if fi.Mode() != mode { s.Skipf("file mode %O is not %O", fi.Mode(), mode) } } elvish-0.21.0/pkg/testutil/dedent.go000066400000000000000000000013361465720375400173340ustar00rootroot00000000000000package testutil import ( "fmt" "strings" ) // Dedent removes an optional leading newline, and removes the indentation // present in the first line from all subsequent non-empty lines. // // Dedent panics if any non-empty line does not start with the same indentation // as the first line. func Dedent(text string) string { lines := strings.Split(strings.TrimPrefix(text, "\n"), "\n") line0 := lines[0] indent := line0[:len(line0)-len(strings.TrimLeft(lines[0], " \t"))] for i, line := range lines { if !strings.HasPrefix(line, indent) && line != "" { panic(fmt.Sprintf("line %d is not empty but doesn't start with %q", i, indent)) } lines[i] = strings.TrimPrefix(line, indent) } return strings.Join(lines, "\n") } elvish-0.21.0/pkg/testutil/dedent_test.go000066400000000000000000000013641465720375400203740ustar00rootroot00000000000000package testutil import "testing" var dedentTests = []struct { name string in string out string }{ { name: "no leading newline, no trailing newline", in: " \n foo\n bar", out: "\n foo\nbar", }, { name: "leading newline, no trailing newline", in: ` a b c`, out: "a\n b\nc", }, { name: "leading newline and trailing newline", in: ` a b c `, out: "a\n b\nc\n", }, } func TestDedent(t *testing.T) { for _, tc := range dedentTests { got := Dedent(tc.in) if got != tc.out { t.Errorf("Dedent(%q) -> %q, want %q", tc.in, got, tc.out) } } } func TestDedentPanicsOnBadInput(t *testing.T) { x := Recover(func() { Dedent(` a b`) }) if x == nil { t.Errorf("Dedent did not panic") } } elvish-0.21.0/pkg/testutil/fs.go000066400000000000000000000000521465720375400164730ustar00rootroot00000000000000package testutil type FS map[string]File elvish-0.21.0/pkg/testutil/recover.go000066400000000000000000000001441465720375400175320ustar00rootroot00000000000000package testutil func Recover(f func()) (r any) { defer func() { r = recover() }() f() return } elvish-0.21.0/pkg/testutil/recover_test.go000066400000000000000000000003551465720375400205750ustar00rootroot00000000000000package testutil import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestRecover(t *testing.T) { tt.Test(t, Recover, Args(func() {}).Rets(nil), Args(func() { panic("unreachable") }).Rets("unreachable"), ) } elvish-0.21.0/pkg/testutil/scaled.go000066400000000000000000000012351465720375400173220ustar00rootroot00000000000000package 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) * TestTimeScale()) } // TestTimeScale parses $E:ELVISH_TEST_TIME_SCALE, defaulting to 1 it it's not // set or can't be parsed as a float64. func TestTimeScale() 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.21.0/pkg/testutil/scaled_test.go000066400000000000000000000020261465720375400203600ustar00rootroot00000000000000package 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.21.0/pkg/testutil/set.go000066400000000000000000000001611465720375400166570ustar00rootroot00000000000000package testutil func Set[T any](c Cleanuper, p *T, v T) { old := *p *p = v c.Cleanup(func() { *p = old }) } elvish-0.21.0/pkg/testutil/set_test.go000066400000000000000000000004251465720375400177210ustar00rootroot00000000000000package testutil import "testing" func TestSet(t *testing.T) { c := &cleanuper{} s := "old" Set(c, &s, "new") if s != "new" { t.Errorf("After Set, s = %q, want %q", s, "new") } c.runCleanups() if s != "old" { t.Errorf("After Set, s = %q, want %q", s, "old") } } elvish-0.21.0/pkg/testutil/temp_env.go000066400000000000000000000013171465720375400177050ustar00rootroot00000000000000package 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.21.0/pkg/testutil/temp_env_test.go000066400000000000000000000030131465720375400207370ustar00rootroot00000000000000package 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.21.0/pkg/testutil/testdir.go000066400000000000000000000134511465720375400175500ustar00rootroot00000000000000package testutil import ( "errors" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strings" "time" "src.elv.sh/pkg/env" "src.elv.sh/pkg/must" ) // 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.Chdir(dir) c.Cleanup(func() { must.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]any // 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) { ApplyDirIn(dir, "") } // ApplyDirIn creates the given filesystem layout in a given directory. func ApplyDirIn(dir Dir, root string) { for name, file := range dir { path := filepath.Join(root, name) switch file := file.(type) { case string: must.OK(os.WriteFile(path, []byte(file), 0644)) case File: must.OK(os.WriteFile(path, []byte(file.Content), file.Perm)) case Dir: must.OK(os.MkdirAll(path, 0755)) ApplyDirIn(file, path) default: panic(fmt.Sprintf("file is neither string, Dir, or Symlink: %v", file)) } } } // fs.FS implementation for Dir. func (dir Dir) Open(name string) (fs.File, error) { if !fs.ValidPath(name) { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} } if name == "." { return newFsDir(".", dir), nil } currentDir := dir currentName := name for { first, rest, moreLevels := strings.Cut(currentName, "/") file, ok := currentDir[first] if !ok { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } if !moreLevels { return newFsFileOrDir(name, file), nil } if nextDir, ok := file.(Dir); ok { currentDir = nextDir currentName = rest } else { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } } } func newFsFileOrDir(name string, x any) fs.File { switch x := x.(type) { case Dir: return newFsDir(name, x) case File: return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x.Content)} case string: return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x)} default: panic(fmt.Sprintf("file is neither string, File or Dir: %v", x)) } } func newFsFileInfo(basename string, x any) fs.FileInfo { switch x := x.(type) { case Dir: return dirInfo{basename} case File: return fileInfo{basename, x.Perm, len(x.Content)} case string: return fileInfo{basename, 0o644, len(x)} default: panic(fmt.Sprintf("file is neither string, File or Dir: %v", x)) } } type fsDir struct { info dirInfo readErr error entries []fs.DirEntry } var errIsDir = errors.New("is a directory") func newFsDir(name string, dir Dir) *fsDir { info := dirInfo{path.Base(name)} readErr := &fs.PathError{Op: "read", Path: name, Err: errIsDir} entries := make([]fs.DirEntry, 0, len(dir)) for name, file := range dir { entries = append(entries, fs.FileInfoToDirEntry(newFsFileInfo(name, file))) } return &fsDir{info, readErr, entries} } func (fd *fsDir) Stat() (fs.FileInfo, error) { return fd.info, nil } func (fd *fsDir) Read([]byte) (int, error) { return 0, fd.readErr } func (fd *fsDir) Close() error { return nil } func (fd *fsDir) ReadDir(n int) ([]fs.DirEntry, error) { if n <= 0 || (n >= len(fd.entries) && len(fd.entries) != 0) { ret := fd.entries fd.entries = nil return ret, nil } if len(fd.entries) == 0 { return nil, io.EOF } ret := fd.entries[:n] fd.entries = fd.entries[n:] return ret, nil } type dirInfo struct{ basename string } var t0 = time.Unix(0, 0).UTC() func (di dirInfo) Name() string { return di.basename } func (dirInfo) Size() int64 { return 0 } func (dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o755 } func (dirInfo) ModTime() time.Time { return t0 } func (dirInfo) IsDir() bool { return true } func (dirInfo) Sys() any { return nil } type fsFile struct { info fileInfo *strings.Reader } func (ff fsFile) Stat() (fs.FileInfo, error) { return ff.info, nil } func (ff fsFile) Close() error { return nil } type fileInfo struct { basename string perm fs.FileMode size int } func (fi fileInfo) Name() string { return fi.basename } func (fi fileInfo) Size() int64 { return int64(fi.size) } func (fi fileInfo) Mode() fs.FileMode { return fi.perm } func (fileInfo) ModTime() time.Time { return t0 } func (fileInfo) IsDir() bool { return false } func (fileInfo) Sys() any { return nil } elvish-0.21.0/pkg/testutil/testdir_nonwindows_test.go000066400000000000000000000014221465720375400230670ustar00rootroot00000000000000//go: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.21.0/pkg/testutil/testdir_test.go000066400000000000000000000127101465720375400206040ustar00rootroot00000000000000package testutil import ( "io" "io/fs" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" "src.elv.sh/pkg/must" "src.elv.sh/pkg/tt" ) 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") } var It = tt.It func TestDirAsFS(t *testing.T) { dir := Dir{ "d": Dir{ "x": "this is file d/x", "y": "this is file d/y", }, "a": "this is file a", "b": File{Perm: 0o777, Content: "this is file b"}, } // fs.WalkDir exercises a large subset of the fs.FS API. entries := make(map[string]string) fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, err error) error { must.OK(err) entries[path] = fs.FormatFileInfo(must.OK1(d.Info())) return nil }) wantEntries := map[string]string{ ".": "drwxr-xr-x 0 1970-01-01 00:00:00 ./", "a": "-rw-r--r-- 14 1970-01-01 00:00:00 a", "b": "-rwxrwxrwx 14 1970-01-01 00:00:00 b", "d": "drwxr-xr-x 0 1970-01-01 00:00:00 d/", "d/x": "-rw-r--r-- 16 1970-01-01 00:00:00 x", "d/y": "-rw-r--r-- 16 1970-01-01 00:00:00 y", } if diff := cmp.Diff(wantEntries, entries); diff != "" { t.Errorf("DirEntry map from walking the FS: (-want +got):\n%s", diff) } // Direct file access is not exercised by fs.WalkDir (other than to "."), so // test those too. readFile := func(name string) (string, error) { bs, err := fs.ReadFile(dir, name) return string(bs), err } tt.Test(t, tt.Fn(readFile).Named("readFile"), It("supports accessing file in root"). Args("a"). Rets("this is file a", error(nil)), It("supports accessing file backed by a File struct"). Args("b"). Rets("this is file b", error(nil)), It("supports accessing file in subdirectory"). Args("d/x"). Rets("this is file d/x", error(nil)), It("errors if file doesn't exist"). Args("d/bad"). Rets("", &fs.PathError{Op: "open", Path: "d/bad", Err: fs.ErrNotExist}), It("errors if a directory component of the path doesn't exist"). Args("badd/x"). Rets("", &fs.PathError{Op: "open", Path: "badd/x", Err: fs.ErrNotExist}), It("errors if a directory component of the path is a file"). Args("a/x"). Rets("", &fs.PathError{Op: "open", Path: "a/x", Err: fs.ErrNotExist}), It("can open but not read a directory"). Args("d"). Rets("", &fs.PathError{Op: "read", Path: "d", Err: errIsDir}), It("errors if path is invalid"). Args("/d"). Rets("", &fs.PathError{Op: "open", Path: "/d", Err: fs.ErrInvalid}), ) // fs.WalkDir calls ReadDir with -1. Also exercise the code for reading // piece by piece. file := must.OK1(dir.Open(".")).(fs.ReadDirFile) rootEntries := make(map[string]string) for { es, err := file.ReadDir(1) if err != nil { if err == io.EOF { break } panic(err) } rootEntries[es[0].Name()] = fs.FormatFileInfo(must.OK1(es[0].Info())) } wantRootEntries := map[string]string{ "a": "-rw-r--r-- 14 1970-01-01 00:00:00 a", "b": "-rwxrwxrwx 14 1970-01-01 00:00:00 b", "d": "drwxr-xr-x 0 1970-01-01 00:00:00 d/", } if diff := cmp.Diff(wantRootEntries, rootEntries); diff != "" { t.Errorf("DirEntry map from reading the root piece by piece: (-want +got):\n%s", diff) } // Cover the Sys method of the two FileInfo implementations. must.OK1(must.OK1(dir.Open("d")).Stat()).Sys() must.OK1(must.OK1(dir.Open("a")).Stat()).Sys() } 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.21.0/pkg/testutil/testutil.go000066400000000000000000000006451465720375400177500ustar00rootroot00000000000000// 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()) } // Skipper wraps the Skipf method. It is a subset of [testing.TB], thus // satisfied by [*testing.T] and [*testing.B]. type Skipper interface { Skipf(format string, args ...any) } elvish-0.21.0/pkg/testutil/testutil_test.go000066400000000000000000000003351465720375400210030ustar00rootroot00000000000000package 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.21.0/pkg/testutil/umask.go000066400000000000000000000003001465720375400171770ustar00rootroot00000000000000package 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.21.0/pkg/testutil/umask_unix.go000066400000000000000000000001321465720375400202450ustar00rootroot00000000000000//go:build unix package testutil import "golang.org/x/sys/unix" var umask = unix.Umask elvish-0.21.0/pkg/testutil/umask_windows.go000066400000000000000000000000631465720375400207570ustar00rootroot00000000000000package testutil func umask(int) int { return 0 } elvish-0.21.0/pkg/transcript/000077500000000000000000000000001465720375400160535ustar00rootroot00000000000000elvish-0.21.0/pkg/transcript/transcript.go000066400000000000000000000326321465720375400206010ustar00rootroot00000000000000// Package transcript contains utilities for working with Elvish transcripts. // // # Basic syntax // // In its most basic form, a transcript consists of a series of code entered // after a prompt, each followed by the resulting output: // // ~> echo foo // foo // ~> echo lorem // echo ipsum // lorem // ipsum // // A line starting with a prompt (as defined by [PromptPattern]) is considered // to start code; code extends to further lines that are indented to align with // the prompt. The other lines are considered output. // // # Headings and sessions // // Two levels of headings are supported: "# h1 #" and "## h2 ##". They split a // transcript into a tree of multiple sessions, and the titles become their // names. // // For example, suppose that a.elvts contains the following content: // // ~> echo hello // hello // // # foo # // // ~> foo // something is done // // # bar # // // ## 1 ## // ~> bar 1 // something is 1 done // // ## 2 ## // ~> bar 2 // something is 2 done // // This file contains the following tree: // // a.elvts // foo // bar // 1 // 2 // // Leading and trailing empty lines are stripped from a session, but internal // empty lines are kept intact. This also applies to transcripts with no // headings (and thus consisting of exactly one session). // // # Comments and directives // // A line starting with "// " or consisting of 2 or more "/"s and nothing else // is a comment. Comments are ignored and can appear anywhere, except that they // can't interrupt multi-line code. // // A line starting with "//" but is not a comment is a directive. Directives can // only appear at the beginning of a session, possibly after other directives, // comments or empty lines. // // # Sessions in .elv files // // An .elv file may contain elvdocs for their variables or functions, which in // turn may contain examples given as elvish-transcript code blocks. // // Each of those code block is considered a transcript, named // $filename/$symbol/$name, where $name is the additional words after the // "elvish-transcript" in the opening fence, defaulting to an empty string. // // File-level directives and symbol-level directives starting with "#//" are // supported. // // As an example, suppose a.elv contains the following content: // // #//dir1 // // #//dir2 // # Does something. // # // # Example: // # // # ```elvish-transcript // # ~> foo // # something is done // # ``` // fn foo {|| } // // # Does something depending on argument. // # // # Example: // # // # ```elvish-transcript 1 // # ~> bar 1 // # something 1 is done // # ``` // # // # Another example: // # // # ```elvish-transcript 2 // # ~> bar 2 // # something 2 is done // # ``` // fn bar {|x| } // // This creates the following tree: // // a.elv // foo // unnamed // bar // 1 // 2 // // These transcripts can also contain headings, which split them into further // smaller sessions. package transcript import ( "bufio" "errors" "fmt" "io" "io/fs" "path/filepath" "regexp" "strings" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/md" "src.elv.sh/pkg/strutil" ) // Node is the result of parsing transcripts. It can represent an .elvts file, a // elvish-transcript block within the elvdoc of an .elv file, or an section // within them started by a header. type Node struct { Name string Directives []string Interactions []Interaction Children []*Node // [LineFrom, LineTo) LineFrom, LineTo int } // ParseFromFS scans fsys recursively for .elv and .elvts files, and // extract transcript sessions from them. func ParseFromFS(fsys fs.FS) ([]*Node, error) { var nodes []*Node err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } parseNode := Parse switch filepath.Ext(path) { case ".elv": parseNode = parseElv case ".elvts": default: return nil } file, err := fsys.Open(path) if err != nil { return err } node, err := parseNode(path, file) if err != nil { return err } nodes = append(nodes, node) return nil }) return nodes, err } func readAllLines(r io.Reader) ([]string, error) { scanner := bufio.NewScanner(r) var lines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } return lines, scanner.Err() } // Scans the elvdoc in an Elvish source file for elvish-transcript blocks and // parses each one similar to an .elvts file. Each block becomes a [Node] on its // own, named like "foo.elv/symbol/code-fence-info" or "foo.elv/symbol" (if // fence info is empty). func parseElv(filename string, r io.Reader) (*Node, error) { docs, err := elvdoc.Extract(r, "") if err != nil { return nil, fmt.Errorf("parse %s for elvdoc: %w", filename, err) } fileNode := &Node{ Name: filename, LineFrom: 1, // LineTo will be updated as children get added } if docs.File != nil { fileNode.Directives = testDirectivesFromElvdoc(docs.File.Directives) } parseEntries := func(entries []elvdoc.Entry) error { for _, entry := range entries { codec := transcriptExtractor{filename, entry.LineNo - 1, nil, nil} md.Render(entry.Content, &codec) if codec.err != nil { return codec.err } symbolNode := &Node{ Name: entry.Name, Directives: testDirectivesFromElvdoc(entry.Directives), Children: codec.nodes, LineFrom: entry.LineNo, LineTo: entry.LineNo + strings.Count(entry.Content, "\n"), } fileNode.Children = append(fileNode.Children, symbolNode) if fileNode.LineTo < symbolNode.LineTo { fileNode.LineTo = symbolNode.LineTo } } return nil } err = parseEntries(docs.Fns) if err != nil { return nil, err } err = parseEntries(docs.Vars) if err != nil { return nil, err } return fileNode, nil } func testDirectivesFromElvdoc(directives []string) []string { var testDirectives []string for _, directive := range directives { if testDirective, ok := strings.CutPrefix(directive, "//"); ok { testDirectives = append(testDirectives, testDirective) } } return testDirectives } // A [md.Codec] implementation that extracts elvish-transcript code blocks from // an elvdoc block as sessions. type transcriptExtractor struct { filename string lineNoOffset int nodes []*Node err error } func (e *transcriptExtractor) Do(op md.Op) { if e.err != nil { return } if op.Type == md.OpCodeBlock { if lang, name, _ := strings.Cut(op.Info, " "); lang == "elvish-transcript" { // The first line of the code block is the fence line, add 1 to get // the first line of the actual content. lineNo := e.lineNoOffset + op.LineNo + 1 node, err := parseNode(name, fileLines{e.filename, op.Lines, lineNo}) if err != nil { e.err = err return } e.nodes = append(e.nodes, node) } } } // Parse parses the transcript sessions from an .elvts file. func Parse(path string, r io.Reader) (*Node, error) { lines, err := readAllLines(r) if err != nil { return nil, fmt.Errorf("read %s: %w", path, err) } return parseNode(path, fileLines{path, lines, 1}) } // Represents a range of lines from a file. type fileLines struct { filename string lines []string startLineNo int // line number of lines[0] } func (fl *fileLines) describeLine(i int) string { return fmt.Sprintf("%s:%d", fl.filename, fl.lineNo(i)) } func (fl *fileLines) lineNo(i int) int { return i + fl.startLineNo } func (fl *fileLines) slice(i, j int) fileLines { return fileLines{fl.filename, fl.lines[i:j], fl.lineNo(i)} } // Parses a single node. This could be an .elvts file, or part of an .elv file. func parseNode(name string, fl fileLines) (*Node, error) { // Path from root to current node. Index corresponds to level, so // nodeStack[0] is the root, nodeStack[1] is the currently active h1, and // so on. nodeStack := []*Node{{Name: name, LineFrom: fl.lineNo(0)}} for i := 0; i < len(fl.lines); { if title, level, ok := parseHeading(fl.lines[i]); ok { // Consume a heading line. This branch will always be entered with // the possible exception of the first iteration, because the // condition is the terminating condition of the loop below to find // which lines to parse as a Session. if level > len(nodeStack) { return nil, fmt.Errorf("%s: h%d before h%d", fl.describeLine(i), level, level-1) } node := &Node{Name: title, LineFrom: fl.lineNo(i)} parent := nodeStack[level-1] parent.Children = append(parent.Children, node) // Terminate all nodes that are at the new node's level or a deeper // level. for _, n := range nodeStack[level:] { n.LineTo = fl.lineNo(i) } // Remove terminated nodes and push new node. nodeStack = append(nodeStack[:level], node) // We're done with this line. i++ } // Consume all lines to the next heading, and parse it as a Session to // attach to the current node. var j int for j = i + 1; j < len(fl.lines); j++ { if _, _, isHeading := parseHeading(fl.lines[j]); isHeading { break } } err := parseSession(nodeStack[len(nodeStack)-1], fl.slice(i, j)) i = j if err != nil { return nil, err } } // Nodes that are still active now terminate at the EOF. for _, n := range nodeStack { n.LineTo = fl.lineNo(len(fl.lines)) } return nodeStack[0], nil } func parseHeading(line string) (title string, level int, ok bool) { if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, " #") { return line[2 : len(line)-2], 1, true } else if strings.HasPrefix(line, "## ") && strings.HasSuffix(line, " ##") { return line[3 : len(line)-3], 2, true } else if strings.HasPrefix(line, "### ") && strings.HasSuffix(line, " ###") { return line[4 : len(line)-4], 3, true } else { return "", 0, false } } // Interaction represents a single REPL interaction - user input followed by the // shell's output. Prompt is never empty. type Interaction struct { Prompt string Code string // [CodeLineFrom, CodeLineTo) identifies the range of code lines. CodeLineFrom, CodeLineTo int Output string // [OutputLineFrom, OutputlineTo) identifies the range of output lines, // excluding any leading and trailing comment lines. OutputLineFrom, OutputLineTo int } // PromptAndCode returns prompt and code concatenated, with spaces prepended to // continuation lines in Code to align with the first line. func (i Interaction) PromptAndCode() string { if i.Code == "" { return i.Prompt } lines := strings.Split(i.Code, "\n") var sb strings.Builder sb.WriteString(i.Prompt + lines[0]) continuation := strings.Repeat(" ", len(i.Prompt)) for _, line := range lines[1:] { sb.WriteString("\n" + continuation + line) } return sb.String() } // PromptPattern defines how to match prompts, used to determine which lines // start the code part of an interaction. var PromptPattern = regexp.MustCompile(`^[~/][^ ]*> `) var ( errFirstLineDoesntHavePrompt = errors.New("first non-comment line of a session doesn't have prompt") errDirectiveOnlyAllowedAtStartOfSession = errors.New("directive only allowed at start of a session") ) // Parses a session into n. Mutates n.Directives and n.Interactions on success. func parseSession(n *Node, fl fileLines) error { lines := fl.lines // Process leading empty lines, comment lines and directive lines. var directives []string start := 0 for ; start < len(lines); start++ { if lines[start] == "" || isComment(lines[start]) { // do nothing } else if directive, ok := parseDirective(lines[start]); ok { directives = append(directives, directive) } else { break } } if start < len(lines) && !PromptPattern.MatchString(lines[start]) { return fmt.Errorf("%s: %w", fl.describeLine(start), errFirstLineDoesntHavePrompt) } // Remove trailing empty lines and comment lines. for len(lines) > 0 && (lines[len(lines)-1] == "" || isComment(lines[len(lines)-1])) { lines = lines[:len(lines)-1] } // Parse interactions. var interactions []Interaction for i := start; i < len(lines); { codeLineFrom := fl.lineNo(i) // Consume the first code line. prompt := PromptPattern.FindString(lines[i]) code := []string{lines[i][len(prompt):]} i++ // Consume continuation code lines. continuation := strings.Repeat(" ", len(prompt)) for i < len(lines) && strings.HasPrefix(lines[i], continuation) { code = append(code, lines[i][len(continuation):]) i++ } codeLineTo := fl.lineNo(i) // Ignore comment lines between code and output. for i < len(lines) && isComment(lines[i]) { i++ } // Consume output lines, ignoring internal and trailing comment lines. var output []string outputLineFrom := fl.lineNo(i) outputLineTo := fl.lineNo(i) for i < len(lines) && !PromptPattern.MatchString(lines[i]) { if _, ok := parseDirective(lines[i]); ok { return fmt.Errorf("%s: %w", fl.describeLine(i), errDirectiveOnlyAllowedAtStartOfSession) } else if isComment(lines[i]) { // Do nothing } else { output = append(output, lines[i]) outputLineTo = fl.lineNo(i + 1) } i++ } interactions = append(interactions, Interaction{ prompt, // Code doesn't include the trailing newline, so a simple // strings.Join is appropriate. strings.Join(code, "\n"), codeLineFrom, codeLineTo, strutil.JoinLines(output), outputLineFrom, outputLineTo}) } n.Directives = directives n.Interactions = interactions return nil } var slashOnlyCommentPattern = regexp.MustCompile(`^///*$`) func isComment(line string) bool { return strings.HasPrefix(line, "// ") || slashOnlyCommentPattern.MatchString(line) } func parseDirective(line string) (string, bool) { if strings.HasPrefix(line, "//") && !isComment(line) { return line[2:], true } return "", false } elvish-0.21.0/pkg/transcript/transcript_test.go000066400000000000000000000201041465720375400216270ustar00rootroot00000000000000package transcript_test import ( "testing" "src.elv.sh/pkg/testutil" . "src.elv.sh/pkg/transcript" "src.elv.sh/pkg/tt" ) type Dir = testutil.Dir var ( It = tt.It Dedent = testutil.Dedent ) func TestParseSessionsInFS(t *testing.T) { tt.Test(t, ParseFromFS, // How sessions are discovered, in both .elv and .elvts files. It("scans .elv and .elvts files recursively, ignoring other files"). Args(Dir{ "d1": Dir{ "foo.elv": Dedent(` # ~~~elvish-transcript # ~> echo foo # foo # ~~~ fn x {|| } `), "ignored.txt": "", }, "d2": Dir{ "bar.elvts": Dedent(` ~> echo bar bar `), }, "ignored.go": "package a", }). Rets([]*Node{ { "d1/foo.elv", nil, nil, []*Node{{ "x", nil, nil, []*Node{{"", nil, []Interaction{{"~> ", "echo foo", 2, 3, "foo\n", 3, 4}}, nil, 2, 4}}, 1, 5}}, 1, 5, }, {"d2/bar.elvts", nil, []Interaction{{"~> ", "echo bar", 1, 2, "bar\n", 2, 3}}, nil, 1, 3}, }), // .elv file-specific handling It("extracts all elvish-transcript code blocks from .elv files"). Args(oneFile("a.elv", ` # ~~~elvish-transcript # ~> f 1 # 1 # ~~~ # # ~~~elvish-transcript # ~> f 2 # 2 # ~~~ fn f {|| } # ~~~elvish-transcript # ~> echo $v # foo # ~~~ var v `)). Rets([]*Node{ {"a.elv", nil, nil, []*Node{ {"f", nil, nil, []*Node{ {"", nil, []Interaction{{"~> ", "f 1", 2, 3, "1\n", 3, 4}}, nil, 2, 4}, {"", nil, []Interaction{{"~> ", "f 2", 7, 8, "2\n", 8, 9}}, nil, 7, 9}, }, 1, 10}, {"$v", nil, nil, []*Node{ {"", nil, []Interaction{{"~> ", "echo $v", 13, 14, "foo\n", 14, 15}}, nil, 13, 15}, }, 12, 16}, }, 1, 16}, }, error(nil)), It("uses fields after elvish-transcript in session name in .elv files"). Args(oneFile("a.elv", ` # ~~~elvish-transcript title # ~> echo foo # foo # ~~~ fn x {|| } `)). Rets([]*Node{ {"a.elv", nil, nil, []*Node{ {"x", nil, nil, []*Node{ {"title", nil, []Interaction{{"~> ", "echo foo", 2, 3, "foo\n", 3, 4}}, nil, 2, 4}, }, 1, 5}, }, 1, 5}, }), It("supports file-level and symbol-level directives"). Args(oneFile("a.elv", ` #//file1 #//file2 #//symbol1 #//symbol2 # ~~~elvish-transcript title # ~> echo foo # foo # ~~~ fn x {|| } `)). Rets([]*Node{ {"a.elv", []string{"file1", "file2"}, nil, []*Node{ {"x", []string{"symbol1", "symbol2"}, nil, []*Node{ {"title", nil, []Interaction{{"~> ", "echo foo", 7, 8, "foo\n", 8, 9}}, nil, 7, 9}, }, 6, 10}, }, 1, 10}, }), It("processes each code block in .elv files like a .elvts file"). Args(oneFile("a.elv", ` # ~~~elvish-transcript # # ~> nop top # # # h1 # # # ~> nop h1 # # ## h2 ## # ~> nop h2 fn x { } `)). Rets([]*Node{{ "a.elv", nil, nil, []*Node{{ "x", nil, nil, []*Node{{ "", nil, []Interaction{{"~> ", "nop top", 3, 4, "", 4, 4}}, []*Node{{ "h1", nil, []Interaction{{"~> ", "nop h1", 7, 8, "", 8, 8}}, []*Node{{ "h2", nil, []Interaction{{"~> ", "nop h2", 10, 11, "", 11, 11}}, nil, 9, 11, }}, 5, 11, }}, 2, 11, }}, 1, 11, }}, 1, 11, }}, error(nil)), // Session splitting It("strips leading and trailing newlines in sessions in .elvts files"). Args(oneFile("a.elvts", ` # h1 # ~> echo foo foo `)). Rets([]*Node{{ "a.elvts", nil, nil, []*Node{{ "h1", nil, []Interaction{{"~> ", "echo foo", 4, 5, "foo\n", 5, 6}}, nil, 1, 8, }}, 1, 8, }}), It("organizes nodes into a tree"). Args(oneFile("a.elvts", ` ~> nop top level # section 1 # ~> nop in section 1 ## subsection 1.1 ## ~> nop in subsection 1.1 ## subsection 1.2 ## ~> nop in subsection 1.2 # section 2 # ~> nop in section 2 `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{{"~> ", "nop top level", 1, 2, "", 2, 2}}, []*Node{ { "section 1", nil, []Interaction{{"~> ", "nop in section 1", 4, 5, "", 5, 5}}, []*Node{ {"subsection 1.1", nil, []Interaction{{"~> ", "nop in subsection 1.1", 7, 8, "", 8, 8}}, nil, 6, 9}, {"subsection 1.2", nil, []Interaction{{"~> ", "nop in subsection 1.2", 10, 11, "", 11, 11}}, nil, 9, 12}, }, 3, 12, }, { "section 2", nil, []Interaction{{"~> ", "nop in section 2", 13, 14, "", 14, 14}}, nil, 12, 14, }, }, 1, 14, }}, error(nil)), It("ignores comment lines in .elvts files"). Args(oneFile("a.elvts", ` // some comment before code ~> echo foo; echo bar // some comments before output foo // some comments inside output bar // some comments after output; note that the preceding empty // lines is also stripped `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{{"~> ", "echo foo; echo bar", 2, 3, "foo\nbar\n", 4, 7}}, nil, 1, 10, }}), It("errors if h2 appears before any h1 in .elvts files"). Args(oneFile("a.elvts", ` ## h2 ## `)). Rets([]*Node(nil), errorWithMsg{"a.elvts:1: h2 before h1"}), // How a single session is parsed into (REPL) cycles. Most of the code // path is shared between .elv and .elvts files, so most of the cases // below only test .elvts files. It("supports cycles with multi-line code and output"). Args(oneFile("a.elvts", ` ~> echo foo echo bar foo bar `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{{"~> ", "echo foo\necho bar", 1, 3, "foo\nbar\n", 3, 5}}, nil, 1, 5, }}), It("supports multiple cycles"). Args(oneFile("a.elvts", ` ~> echo foo echo bar foo bar ~> echo lorem echo ipsum lorem ipsum `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{ {"~> ", "echo foo\necho bar", 1, 3, "foo\nbar\n", 3, 5}, {"~> ", "echo lorem\necho ipsum", 5, 7, "lorem\nipsum\n", 7, 9}, }, nil, 1, 9, }}), It("supports cycles with empty output"). Args(oneFile("a.elvts", ` ~> nop ~> nop `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{ {"~> ", "nop", 1, 2, "", 2, 2}, {"~> ", "nop", 2, 3, "", 3, 3}, }, nil, 1, 3, }}), It("supports more complex prompts"). Args(oneFile("a.elvts", ` ~/foo> echo foo foo /opt/bar> echo bar bar `)). Rets([]*Node{{ "a.elvts", nil, []Interaction{ {"~/foo> ", "echo foo", 1, 2, "foo\n", 2, 3}, {"/opt/bar> ", "echo bar", 3, 4, "bar\n", 4, 5}, }, nil, 1, 5, }}), It("supports directives"). Args(oneFile("a.elvts", ` //directive 1 // some comment and some empty lines //directive 2 ~> nop `)). Rets([]*Node{{ "a.elvts", []string{"directive 1", "directive 2"}, []Interaction{{"~> ", "nop", 7, 8, "", 8, 8}}, nil, 1, 8, }}), It("errors when a session in a .elvts file doesn't start with a prompt"). Args(oneFile("a.elvts", ` something ~> echo foo foo `)). Rets([]*Node(nil), errorWithMsg{"a.elvts:2: first non-comment line of a session doesn't have prompt"}), It("errors when a session in a fn elvdoc in a .elv file doesn't start with a prompt"). Args(oneFile("a.elv", ` # ~~~elvish-transcript # something # ~~~ fn x { } `)). Rets([]*Node(nil), errorWithMsg{"a.elv:2: first non-comment line of a session doesn't have prompt"}), It("errors when a session in a var elvdoc in a .elv file doesn't start with a prompt"). Args(oneFile("a.elv", ` # ~~~elvish-transcript # something # ~~~ var x `)). Rets([]*Node(nil), errorWithMsg{"a.elv:2: first non-comment line of a session doesn't have prompt"}), ) } func oneFile(name, content string) Dir { return Dir{name: Dedent(content)} } type errorWithMsg struct{ msg string } func (e errorWithMsg) Match(got tt.RetValue) bool { if gotErr, ok := got.(error); ok { return e.msg == gotErr.Error() } return false } elvish-0.21.0/pkg/tt/000077500000000000000000000000001465720375400143115ustar00rootroot00000000000000elvish-0.21.0/pkg/tt/cmpopt.go000066400000000000000000000017051465720375400161450ustar00rootroot00000000000000package tt import ( "math/big" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "src.elv.sh/pkg/persistent/hashmap" "src.elv.sh/pkg/persistent/vector" ) // CommonCmpOpt is cmp.Option shared between tt and evaltest. var CommonCmpOpt = cmp.Options([]cmp.Option{ cmp.Transformer("transformList", transformList), cmp.Transformer("transformMap", transformMap), cmp.Comparer(func(x, y *big.Int) bool { return x.Cmp(y) == 0 }), cmp.Comparer(func(x, y *big.Rat) bool { return x.Cmp(y) == 0 }), }) var cmpopt = cmp.Options([]cmp.Option{ cmpopts.EquateErrors(), CommonCmpOpt, }) func transformList(l vector.Vector) []any { res := make([]any, 0, l.Len()) for it := l.Iterator(); it.HasElem(); it.Next() { res = append(res, it.Elem()) } return res } func transformMap(m hashmap.Map) map[any]any { res := make(map[any]any, m.Len()) for it := m.Iterator(); it.HasElem(); it.Next() { k, v := it.Elem() res[k] = v } return res } elvish-0.21.0/pkg/tt/tt.go000066400000000000000000000144241465720375400152740ustar00rootroot00000000000000// Package tt supports table-driven tests with little boilerplate. // // A typical use of this package looks like this: // // // Function being tested // func Neg(i int) { return -i } // // func TestNeg(t *testing.T) { // Test(t, Neg, // // Unnamed test case // Args(1).Rets(-1), // // Named test case // It("returns 0 for 0").Args(0).Rets(0), // ) // } package tt import ( "fmt" "path/filepath" "reflect" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" ) // Case represents a test case. It has setter methods that augment and return // itself, so they can be chained like It(...).Args(...).Rets(...). type Case struct { fileAndLine string desc string args []any retsMatchers [][]any } // It returns a Case with the given text description. func It(desc string) *Case { return &Case{fileAndLine: fileAndLine(2), desc: desc} } // Args is equivalent to It("").args(...). It is useful when the test case is // trivial and doesn't need a description; for more complex or interesting test // cases, use [It] instead. func Args(args ...any) *Case { return &Case{fileAndLine: fileAndLine(2), args: args} } func fileAndLine(skip int) string { _, filename, line, _ := runtime.Caller(skip) return fmt.Sprintf("%s:%d", filepath.Base(filename), line) } // Args modifies the Case to pass the given arguments. It returns the receiver. func (c *Case) Args(args ...any) *Case { c.args = args return c } // Rets modifies the Case to expect the given return 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 ...any) *Case { c.retsMatchers = append(c.retsMatchers, matchers) return c } // FnDescriptor describes a function to test. It has setter methods that augment // and return itself, so they can be chained like // Fn(...).Named(...).ArgsFmt(...). type FnDescriptor struct { name string body any argsFmt string } // Fn creates a FnDescriptor for the given function. func Fn(body any) *FnDescriptor { return &FnDescriptor{body: body} } // Named sets the name of the function. This is only necessary for methods and // local closures; package-level functions will have their name automatically // inferred via reflection. It returns the receiver. func (fn *FnDescriptor) Named(name string) *FnDescriptor { fn.name = name return fn } // ArgsFmt sets the string for formatting arguments in test error messages. It // returns the receiver. func (fn *FnDescriptor) ArgsFmt(s string) *FnDescriptor { fn.argsFmt = s return fn } // Test tests fn against the given Case instances. // // The fn argument may be the function itself or an explicit [FnDescriptor], the // former case being equivalent to passing Fn(fn). func Test(t *testing.T, fn any, tests ...*Case) { testInner[*testing.T](t, fn, tests...) } // Instead of using [*testing.T] directly, the inner implementation uses two // interfaces so that it can be mocked. We need two interfaces because // type parameters can't refer to the type itself. type testRunner[T subtestRunner] interface { Helper() Run(name string, f func(t T)) bool } type subtestRunner interface { Errorf(format string, args ...any) } func testInner[T subtestRunner](t testRunner[T], fn any, tests ...*Case) { t.Helper() var fnd *FnDescriptor switch fn := fn.(type) { case *FnDescriptor: fnd = &FnDescriptor{} *fnd = *fn default: fnd = Fn(fn) } if fnd.name == "" { // Use reflection to discover the function's name. name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() // Tests are usually restricted to functions in the same package, so // elide the package name. if i := strings.LastIndexByte(name, '.'); i != -1 { name = name[i+1:] } fnd.name = name } for _, test := range tests { t.Run(test.desc, func(t T) { rets := call(fnd.body, test.args) for _, retsMatcher := range test.retsMatchers { if !match(retsMatcher, rets) { var args string if fnd.argsFmt == "" { args = sprintArgs(test.args...) } else { args = fmt.Sprintf(fnd.argsFmt, test.args...) } var diff string if len(retsMatcher) == 1 && len(rets) == 1 { diff = cmp.Diff(retsMatcher[0], rets[0], cmpopt) } else { diff = cmp.Diff(retsMatcher, rets, cmpopt) } t.Errorf("%s: %s(%s) returns (-want +got):\n%s", test.fileAndLine, fnd.name, args, diff) } } }) } } // RetValue is an empty interface used in the [Matcher] interface. type RetValue any // Matcher wraps the Match method. // // Values that implement this interface can be passed to [*Case.Rets] to control // the matching algorithm for return values. 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 []any) bool { for i, matcher := range matchers { if !matchOne(matcher, actual[i]) { return false } } return true } func matchOne(m, a any) bool { if m, ok := m.(Matcher); ok { return m.Match(a) } return reflect.DeepEqual(m, a) } func sprintArgs(args ...any) string { var b strings.Builder for i, arg := range args { if i > 0 { b.WriteString(", ") } fmt.Fprint(&b, arg) } return b.String() } func call(fn any, args []any) []any { 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 any argsReflect[i] = reflect.ValueOf(&v).Elem() } else { argsReflect[i] = reflect.ValueOf(arg) } } retsReflect := reflect.ValueOf(fn).Call(argsReflect) rets := make([]any, len(retsReflect)) for i, retReflect := range retsReflect { rets[i] = retReflect.Interface() } return rets } elvish-0.21.0/pkg/tt/tt_test.go000066400000000000000000000047061465720375400163350ustar00rootroot00000000000000package tt import ( "fmt" "strings" "testing" ) // 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 TestTest(t *testing.T) { Test(t, test, It("reports no errors for passing tests"). Args(add, Args(1, 1).Rets(2)). Rets([]testResult{{"", nil}}), It("supports multiple tests"). Args(add, Args(1, 1).Rets(2), Args(1, 2).Rets(3)), It("supports for multiple return values"). Args(addsub, Args(1, 2).Rets(3, -1)). Rets([]testResult{{"", nil}}), It("supports named tests"). Args(add, It("can add 1 and 1").Args(1, 1).Rets(2)). Rets([]testResult{{"can add 1 and 1", nil}}), It("reports error for failed test"). Args(Fn(add).Named("add"), Args(2, 2).Rets(5)). Rets(testResultsMatcher{ {"", []string{"add(2, 2) returns (-want +got):\n"}}, }), It("respects custom argument format strings when reporting errors"). Args(Fn(add).Named("add").ArgsFmt("x = %d, y = %d"), Args(1, 2).Rets(5)). Rets(testResultsMatcher{ {"", []string{"add(x = 1, y = 2) returns (-want +got):\n"}}, }), ) } // An alternative to the exported [Test] that uses a mock test runner that // collects results from all the subtests. func test(fn any, tests ...*Case) []testResult { var tr mockTestRunner testInner[*mockSubtestRunner](&tr, fn, tests...) return tr } // Mock implementations of testRunner and subtestRunner. type testResult struct { Name string Errors []string } type mockTestRunner []testResult func (tr *mockTestRunner) Helper() {} func (tr *mockTestRunner) Run(name string, f func(*mockSubtestRunner)) bool { sr := mockSubtestRunner{name, nil} f(&sr) *tr = append(*tr, testResult(sr)) return len(sr.Errors) == 0 } type mockSubtestRunner testResult func (sr *mockSubtestRunner) Errorf(format string, args ...any) { sr.Errors = append(sr.Errors, fmt.Sprintf(format, args...)) } // Matches []testResult, but doesn't check the exact content of the error // messages, only that they contain a substring. type testResultsMatcher []testResult func (m testResultsMatcher) Match(ret RetValue) bool { results, ok := ret.([]testResult) if !ok { return false } if len(results) != len(m) { return false } for i, result := range results { if result.Name != m[i].Name || len(result.Errors) != len(m[i].Errors) { return false } for i, s := range result.Errors { if !strings.Contains(s, m[i].Errors[i]) { return false } } } return true } elvish-0.21.0/pkg/ui/000077500000000000000000000000001465720375400142775ustar00rootroot00000000000000elvish-0.21.0/pkg/ui/color.go000066400000000000000000000062521465720375400157510ustar00rootroot00000000000000package 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.21.0/pkg/ui/color_test.go000066400000000000000000000024411465720375400170040ustar00rootroot00000000000000package 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.21.0/pkg/ui/key.go000066400000000000000000000126031465720375400154200ustar00rootroot00000000000000package 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 DefaultKey = 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 any) 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.21.0/pkg/ui/key_test.go000066400000000000000000000057151465720375400164650ustar00rootroot00000000000000package 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.21.0/pkg/ui/mark_lines.go000066400000000000000000000044311465720375400167540ustar00rootroot00000000000000package 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 = RuneStylesheet{ // '-': Reverse, // 'x': Stylings(Blue, BgGreen), // } // var text = FromMarkedLines( // "foo bar foobar", stylesheet, // "--- xxx ------" // "lorem ipsum dolar", // ) func MarkLines(args ...any) Text { var tb TextBuilder 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 { tb.WriteText(MarkText(line, stylesheet, style)) i += 2 continue } } } tb.WriteText(T(line)) } return tb.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 tb TextBuilder styleRuns := toRuns(style) for _, styleRun := range styleRuns { i := bytesForFirstNRunes(line, styleRun.n) tb.WriteText(T(line[:i], stylesheet[styleRun.r])) line = line[i:] } if len(line) > 0 { tb.WriteText(T(line)) } return tb.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.21.0/pkg/ui/mark_lines_test.go000066400000000000000000000016041465720375400200120ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestMarkLines(t *testing.T) { stylesheet := RuneStylesheet{ '-': Inverse, 'x': Stylings(FgBlue, BgGreen), } tt.Test(t, MarkLines, Args("foo bar foobar").Rets(T("foo bar foobar")), Args( "foo bar foobar", stylesheet, "--- xxx ------", ).Rets( Concat( T("foo", Inverse), T(" "), T("bar", FgBlue, BgGreen), T(" "), T("foobar", Inverse)), ), Args( "foo bar foobar", stylesheet, "---", ).Rets( Concat( T("foo", Inverse), T(" bar foobar")), ), 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.21.0/pkg/ui/parse_sgr.go000066400000000000000000000072121465720375400166150ustar00rootroot00000000000000package 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 case code == 39: moreStyling = FgDefault case code == 49: moreStyling = BgDefault 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.21.0/pkg/ui/parse_sgr_test.go000066400000000000000000000022741465720375400176570ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/tt" ) func TestParseSGREscapedText(t *testing.T) { tt.Test(t, ParseSGREscapedText, Args("").Rets(Text(nil)), Args("text").Rets(T("text")), Args("\033[1mbold").Rets(T("bold", Bold)), Args("\033[1mbold\033[31mbold red").Rets( Concat(T("bold", Bold), T("bold red", Bold, FgRed))), Args("\033[1mbold\033[;31mred").Rets( Concat(T("bold", Bold), T("red", FgRed))), // Non-SGR CSI sequences are removed. Args("\033[Atext").Rets(T("text")), // Control characters not part of CSI escape sequences are left // untouched. Args("t\x01ext").Rets(T("t\x01ext")), ) } func TestStyleFromSGR(t *testing.T) { tt.Test(t, StyleFromSGR, Args("1").Rets(Style{Bold: true}), // Invalid codes are ignored Args("1;invalid;10000").Rets(Style{Bold: true}), // ANSI colors. Args("31;42").Rets(Style{Fg: Red, Bg: Green}), // ANSI bright colors. Args("91;102"). Rets(Style{Fg: BrightRed, Bg: BrightGreen}), // XTerm 256 color. Args("38;5;1;48;5;2"). Rets(Style{Fg: XTerm256Color(1), Bg: XTerm256Color(2)}), // True colors. Args("38;2;1;2;3;48;2;10;20;30"). Rets(Style{ Fg: TrueColor(1, 2, 3), Bg: TrueColor(10, 20, 30)}), ) } elvish-0.21.0/pkg/ui/style.go000066400000000000000000000044101465720375400157650ustar00rootroot00000000000000package ui import ( "fmt" "strings" ) // NoColor can be set to true to suppress foreground and background colors when // writing text to the terminal. var NoColor bool = false // Style specifies how something (mostly a string) shall be displayed. type Style struct { Fg Color Bg Color Bold bool Dim bool Italic bool Underlined bool Blink bool Inverse bool } // SGRValues returns an array of the individual SGR values for the style. func (s Style) SGRValues() []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.Fg != nil && !NoColor { sgr = append(sgr, s.Fg.fgSGR()) } if s.Bg != nil && !NoColor { sgr = append(sgr, s.Bg.bgSGR()) } return sgr } // SGR returns, for the Style, a string that can be included in an ANSI X3.64 SGR sequence. func (s Style) SGR() string { return strings.Join(s.SGRValues(), ";") } // MergeFromOptions merges all recognized values from a map to the current // Style. func (s *Style) MergeFromOptions(options map[string]any) error { assignColor := func(val any, 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 any, 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.Fg) case "bg-color": need = assignColor(v, &s.Bg) 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.21.0/pkg/ui/style_regions.go000066400000000000000000000032631465720375400175200ustar00rootroot00000000000000package 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.21.0/pkg/ui/style_regions_test.go000066400000000000000000000035051465720375400205560ustar00rootroot00000000000000package ui_test import ( "reflect" "testing" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/ui" ) var styleRegionsTests = []struct { Name string String string Regions []ui.StylingRegion WantText ui.Text }{ { Name: "empty string and regions", String: "", Regions: nil, WantText: nil, }, { Name: "a single region", String: "foobar", Regions: []ui.StylingRegion{ {r(1, 3), ui.FgRed, 0}, }, WantText: ui.Concat(ui.T("f"), ui.T("oo", ui.FgRed), ui.T("bar")), }, { Name: "multiple continuous regions", String: "foobar", Regions: []ui.StylingRegion{ {r(1, 3), ui.FgRed, 0}, {r(3, 4), ui.FgGreen, 0}, }, WantText: ui.Concat(ui.T("f"), ui.T("oo", ui.FgRed), ui.T("b", ui.FgGreen), ui.T("ar")), }, { Name: "multiple discontinuous regions in wrong order", String: "foobar", Regions: []ui.StylingRegion{ {r(4, 5), ui.FgGreen, 0}, {r(1, 3), ui.FgRed, 0}, }, WantText: ui.Concat(ui.T("f"), ui.T("oo", ui.FgRed), ui.T("b"), ui.T("a", ui.FgGreen), ui.T("r")), }, { Name: "regions with the same starting position but differeng priorities", String: "foobar", Regions: []ui.StylingRegion{ {r(1, 3), ui.FgRed, 0}, {r(1, 2), ui.FgGreen, 1}, }, WantText: ui.Concat(ui.T("f"), ui.T("o", ui.FgGreen), ui.T("obar")), }, { Name: "overlapping regions with different starting positions", String: "foobar", Regions: []ui.StylingRegion{ {r(1, 3), ui.FgRed, 0}, {r(2, 4), ui.FgGreen, 0}, }, WantText: ui.Concat(ui.T("f"), ui.T("oo", ui.FgRed), ui.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 := ui.StyleRegions(test.String, test.Regions) if !reflect.DeepEqual(text, test.WantText) { t.Errorf("got %v, want %v", text, test.WantText) } } } elvish-0.21.0/pkg/ui/style_test.go000066400000000000000000000053251465720375400170320ustar00rootroot00000000000000package ui import ( "testing" "src.elv.sh/pkg/testutil" ) 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"}, }) } func TestStyleSGR_NoColor(t *testing.T) { testutil.Set(t, &NoColor, true) testTextVTString(t, []textVTStringTest{ {T("foo", FgRed), "\033[mfoo"}, {T("foo", BgRed), "\033[mfoo"}, {T("foo", FgRed, BgBlue), "\033[mfoo"}, }) } type mergeFromOptionsTest struct { style Style options map[string]any wantStyle Style wantErr string } var mergeFromOptionsTests = []mergeFromOptionsTest{ // Parsing of each possible key. kv("fg-color", "red", Style{Fg: Red}), kv("bg-color", "red", Style{Bg: 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]any{ "bold": false, "fg-color": "red", }, wantStyle: Style{Dim: true, Fg: Red}, }, // Bad key. { options: map[string]any{"bad": true}, wantErr: "unrecognized option 'bad'", }, // Bad type for color field. { options: map[string]any{"fg-color": true}, wantErr: "value for option 'fg-color' must be a valid color string", }, // Bad type for bool field. { options: map[string]any{"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 any, s Style) mergeFromOptionsTest { return mergeFromOptionsTest{ options: map[string]any{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.21.0/pkg/ui/styledown/000077500000000000000000000000001465720375400163275ustar00rootroot00000000000000elvish-0.21.0/pkg/ui/styledown/styledown.go000066400000000000000000000112601465720375400207060ustar00rootroot00000000000000// Styledown is a simple markup language for representing styled text. // // In the most basic form, Styledown markup consists of alternating text // lines and style lines, where each character in the style line specifies // the style of the character directly above it. For example: // // foobar // ***### // lorem // _____ // // represents two lines: // // 1. "foo" in bold plus "bar" in reverse video // 2. "lorem" in underline // // The following style characters are built-in: // // - space for no style // - * for bold // - _ for underline // - # for reverse video // // This package can be used as a Go library or via Elvish's render-styledown // command (https://elv.sh/ref/builtin.html#render-styledown). // // # Double-width characters // // Characters in text and style lines are matched up using their visual // width, as calculated by [src.elv.sh/pkg/wcwidth.OfRune]. This means that // double-width characters need to have their style character doubled: // // 好 foo // ** ### // // The two style characters must be the same. // // # Configuration stanza // // An optional configuration stanza can follow the text and style lines (the // content stanza), separated by a single newline. It can define additional // style characters like this: // // foobar // rrrGGG // // r fg-red // G inverse fg-green // // Each line consists of the style character and one or more stylings as // recognized by [src.elv.sh/pkg/ui.ParseStyling], separated by whitespaces. The // character must be a single Unicode codepoint and have a visual width of 1. // // The configuration stanza can also contain additional options, and there's // currently just one: // // - no-eol: suppress the newline after the last line // // # Rationale // // Styledown is suitable for authoring a large chunk of styled text when the // exact width and alignment of text need to be preserved. // // For example, it can be used to manually create and edit terminal mockups. In // future it will be used in Elvish's tests for its terminal UI. package styledown import ( "fmt" "strings" "unicode/utf8" "src.elv.sh/pkg/ui" "src.elv.sh/pkg/wcwidth" ) // Render renders Styledown markup. If the markup has parse errors, the error // will start with "line x", where x is a 1-based line number. func Render(s string) (ui.Text, error) { lines := strings.Split(s, "\n") i := 0 for ; i+1 < len(lines) && wcwidth.Of(lines[i]) == wcwidth.Of(lines[i+1]); i += 2 { } contentLines := i if i < len(lines) { if lines[i] != "" { return nil, fmt.Errorf( "line %d: content and configuration stanzas must be separated by a newline", 1+i) } i++ } opts, stylesheet, err := parseConfig(lines[i:], i+1) if err != nil { return nil, err } var tb ui.TextBuilder for i := 0; i < contentLines; i += 2 { if i > 0 { tb.WriteText(ui.T("\n")) } text, style := []rune(lines[i]), []rune(lines[i+1]) for len(text) > 0 { r := text[0] w := wcwidth.OfRune(r) if !same(style[:w]) { return nil, fmt.Errorf( "line %d: inconsistent style %q for multi-width character %q", i+2, string(style[:w]), string(r)) } styling, ok := stylesheet[style[0]] if !ok { return nil, fmt.Errorf( "line %d: unknown style %q", i+2, string(style[0])) } tb.WriteText(ui.T(string(r), styling)) text = text[1:] style = style[w:] } } if !opts.noEOL { tb.WriteText(ui.T("\n")) } return tb.Text(), nil } type options struct { noEOL bool } func parseConfig(lines []string, firstLineNo int) (options, map[rune]ui.Styling, error) { var opts options stylesheet := map[rune]ui.Styling{ ' ': ui.Reset, '*': ui.Bold, '_': ui.Underlined, '#': ui.Inverse, } for i, line := range lines { if line == "" { continue } if line == "no-eol" { opts.noEOL = true continue } // Parse a style character definition. fields := strings.Fields(line) if len(fields) < 2 { return options{}, nil, fmt.Errorf( "line %d: invalid configuration line", i+firstLineNo) } r, _ := utf8.DecodeRuneInString(fields[0]) if string(r) != fields[0] { return options{}, nil, fmt.Errorf( "line %d: style character %q not a single character", i+firstLineNo, fields[0]) } if wcwidth.OfRune(r) != 1 { return options{}, nil, fmt.Errorf( "line %d: style character %q not single-width", i+firstLineNo, fields[0]) } stylingString := strings.Join(fields[1:], " ") styling := ui.ParseStyling(stylingString) if styling == nil { return options{}, nil, fmt.Errorf( "line %d: invalid styling string %q", i+firstLineNo, stylingString) } stylesheet[r] = styling } return opts, stylesheet, nil } func same[T comparable](s []T) bool { for i := 0; i+1 < len(s); i++ { if s[i] != s[i+1] { return false } } return true } elvish-0.21.0/pkg/ui/styledown/styledown_test.elvts000066400000000000000000000052401465720375400224760ustar00rootroot00000000000000~> render-styledown ' foobar ***### lorem _____ '[1..] ▶ [^styled (styled-segment foo &bold) (styled-segment bar &inverse) "\n" (styled-segment lorem &underlined) "\n"] /////////////////////////// # double-width characters # /////////////////////////// ~> render-styledown ' 好 foo ** ### '[1..] ▶ [^styled (styled-segment 好 &bold) ' ' (styled-segment foo &inverse) "\n"] ~> render-styledown ' 好 foo *# ### '[1..] Exception: line 2: inconsistent style "*#" for multi-width character "好" [tty]:1:1-4:6: render-styledown ' 好 foo *# ### '[1..] //////////////////////// # configuration stanza # //////////////////////// ~> render-styledown ' foo bar rrr ggg r fg-red g bg-green '[1..] ▶ [^styled (styled-segment foo &fg-color=red) ' ' (styled-segment bar &bg-color=green) "\n"] ~> render-styledown ' foo *** no-eol'[1..] ▶ [^styled (styled-segment foo &bold)] //////////////////////////////// # Trailing newline is optional # //////////////////////////////// ~> render-styledown ' foobar ***###'[1..] ▶ [^styled (styled-segment foo &bold) (styled-segment bar &inverse) "\n"] ~> render-styledown ' foobar ***### no-eol'[1..] ▶ [^styled (styled-segment foo &bold) (styled-segment bar &inverse)] ////////// # errors # ////////// // Unknown style ~> render-styledown ' foo xxx '[1..] Exception: line 2: unknown style "x" [tty]:1:1-4:6: render-styledown ' foo xxx '[1..] // Empty line between stanzas is required ~> render-styledown ' foo *** no-eol '[1..] Exception: line 3: content and configuration stanzas must be separated by a newline [tty]:1:1-5:6: render-styledown ' foo *** no-eol '[1..] // Unknown option ~> render-styledown ' foo *** unknown-option '[1..] Exception: line 4: invalid configuration line [tty]:1:1-6:6: render-styledown ' foo *** unknown-option '[1..] // Style character is not a single character ~> render-styledown ' foo *** xx fg-red '[1..] Exception: line 4: style character "xx" not a single character [tty]:1:1-6:6: render-styledown ' foo *** xx fg-red '[1..] // Style character is not single-width ~> render-styledown ' foo *** 好 fg-red '[1..] Exception: line 4: style character "好" not single-width [tty]:1:1-6:6: render-styledown ' foo *** 好 fg-red '[1..] // Bad styling string ~> render-styledown ' foo *** x bad '[1..] Exception: line 4: invalid styling string "bad" [tty]:1:1-6:6: render-styledown ' foo *** x bad '[1..] elvish-0.21.0/pkg/ui/styledown/transcripts_test.go000066400000000000000000000003331465720375400222700ustar00rootroot00000000000000package styledown_test import ( "embed" "testing" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts) } elvish-0.21.0/pkg/ui/styling.go000066400000000000000000000151671465720375400163310ustar00rootroot00000000000000package 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.Fg = t.c } func (t setBackground) transform(s *Style) { s.Bg = 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.21.0/pkg/ui/styling_test.go000066400000000000000000000047501465720375400173640ustar00rootroot00000000000000package ui import ( "reflect" "testing" "src.elv.sh/pkg/tt" ) func TestStyleText(t *testing.T) { tt.Test(t, StyleText, // Foreground color Args(T("foo"), FgRed). Rets(Text{&Segment{Style{Fg: Red}, "foo"}}), // Override existing foreground Args(Text{&Segment{Style{Fg: Green}, "foo"}}, FgRed). Rets(Text{&Segment{Style{Fg: Red}, "foo"}}), // Multiple segments Args(Text{ &Segment{Style{}, "foo"}, &Segment{Style{Fg: Green}, "bar"}}, FgRed). Rets(Text{ &Segment{Style{Fg: Red}, "foo"}, &Segment{Style{Fg: Red}, "bar"}, }), // Background color Args(T("foo"), BgRed). Rets(Text{&Segment{Style{Bg: Red}, "foo"}}), // Bold, false -> true Args(T("foo"), Bold). Rets(Text{&Segment{Style{Bold: true}, "foo"}}), // Bold, true -> true Args(Text{&Segment{Style{Bold: true}, "foo"}}, Bold). Rets(Text{&Segment{Style{Bold: true}, "foo"}}), // No Bold, true -> false Args(Text{&Segment{Style{Bold: true}, "foo"}}, NoBold). Rets(Text{&Segment{Style{}, "foo"}}), // No Bold, false -> false Args(T("foo"), NoBold).Rets(T("foo")), // Toggle Bold, true -> false Args(Text{&Segment{Style{Bold: true}, "foo"}}, ToggleBold). Rets(Text{&Segment{Style{}, "foo"}}), // Toggle Bold, false -> true 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. Args(T("foo"), Dim). Rets(Text{&Segment{Style{Dim: true}, "foo"}}), // Italic. Args(T("foo"), Italic). Rets(Text{&Segment{Style{Italic: true}, "foo"}}), // Underlined. Args(T("foo"), Underlined). Rets(Text{&Segment{Style{Underlined: true}, "foo"}}), // Blink. Args(T("foo"), Blink). Rets(Text{&Segment{Style{Blink: true}, "foo"}}), // Inverse. 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.21.0/pkg/ui/text.go000066400000000000000000000145201465720375400156140ustar00rootroot00000000000000package ui import ( "bytes" "fmt" "math/big" "strconv" "strings" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/wcwidth" ) // Text contains of a list of styled Segments. // // When only functions in this package are used to manipulate Text instances, // they will always satisfy the following properties: // // - If the Text is empty, it is nil (not a non-nil slice of size 0). // // - No [Segment] in it has an empty Text field. // // - No two adjacent [Segment] instances have the same [Style]. 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 { if s == "" { return nil } return StyleText(Text{&Segment{Text: s}}, ts...) } // Concat concatenates multiple Text's into one. func Concat(texts ...Text) Text { var tb TextBuilder for _, text := range texts { tb.WriteText(text) } return tb.Text() } // 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.WriteByte(' ') buf.WriteString(s.Repr(indent + 1)) } return fmt.Sprintf("[^styled%s]", buf.String()) } // IterateKeys feeds the function with all valid indices of the styled-text. func (t Text) IterateKeys(fn func(any) 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 any) (any, 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 any) (any, 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 any) (any, 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 { if len(t) == 0 { return nil } // 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 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 TextBuilder for _, seg := range t { subSegs := seg.SplitByRune(r) // Paste the first segment. paste.WriteText(TextFromSegment(subSegs[0])) if len(subSegs) == 1 { // Only one subsegment. Keep the paste active. continue } // Add the paste and reset it. result = append(result, paste.Text()) paste.Reset() // For the subsegments in the middle, just add then as is. for i := 1; i < len(subSegs)-1; i++ { result = append(result, TextFromSegment(subSegs[i])) } // The last segment becomes the new paste. paste.WriteText(TextFromSegment(subSegs[len(subSegs)-1])) } result = append(result, paste.Text()) 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. Any // existing SGR state will be cleared. func (t Text) VTString() string { var sb strings.Builder clean := false for _, seg := range t { sgr := seg.SGR() if sgr == "" { if !clean { sb.WriteString("\033[m") } clean = true } else { if clean { sb.WriteString("\033[" + sgr + "m") } else { sb.WriteString("\033[;" + sgr + "m") } clean = false } sb.WriteString(seg.Text) } if !clean { sb.WriteString("\033[m") } return sb.String() } // TextFromSegment returns a [Text] with just seg if seg.Text is non-empty. // Otherwise it returns nil. func TextFromSegment(seg *Segment) Text { if seg.Text == "" { return nil } return Text{seg} } elvish-0.21.0/pkg/ui/text_builder.go000066400000000000000000000031571465720375400173260ustar00rootroot00000000000000package ui import "strings" // Methods of [TextBuilder] are fully exercised by other functions Concat, so // there are no dedicated tests for it. // TextBuilder can be used to efficiently build a [Text]. The zero value is // ready to use. Do not copy a non-zero TextBuilder. type TextBuilder struct { segs []*Segment style Style text strings.Builder } // WriteText appends t to the TextBuilder. func (tb *TextBuilder) WriteText(t Text) { if len(t) == 0 { return } if tb.style == t[0].Style { // Merge the first segment of t with the pending segment. tb.text.WriteString(t[0].Text) t = t[1:] if len(t) == 0 { return } } // At this point, the first segment of t has a different style than the // pending segment (assuming that t is normal). Add the pending segment if // it's non-empty. if tb.text.Len() > 0 { tb.segs = append(tb.segs, &Segment{tb.style, tb.text.String()}) tb.text.Reset() } // Add all segments from t except the last one. tb.segs = append(tb.segs, t[:len(t)-1]...) // Use the last segment of t as the pending segment. tb.style = t[len(t)-1].Style tb.text.WriteString(t[len(t)-1].Text) } // Text returns the [Text] that has been built. func (tb *TextBuilder) Text() Text { if tb.Empty() { return nil } t := append(Text(nil), tb.segs...) return append(t, &Segment{tb.style, tb.text.String()}) } // Empty returns nothing has been written to the TextBuilder yet. func (tb *TextBuilder) Empty() bool { return len(tb.segs) == 0 && tb.text.Len() == 0 } // Reset resets the TextBuilder to be empty. func (tb *TextBuilder) Reset() { tb.segs = nil tb.style = Style{} tb.text.Reset() } elvish-0.21.0/pkg/ui/text_segment.go000066400000000000000000000074451465720375400173460ustar00rootroot00000000000000package ui import ( "fmt" "math/big" "strings" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/parse" ) // 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 { var sb strings.Builder addColor := func(key string, c Color) { if c != nil { fmt.Fprintf(&sb, " &%s=%s", key, c.String()) } } addBool := func(key string, b bool) { if b { fmt.Fprintf(&sb, " &%s", key) } } addColor("fg-color", s.Fg) addColor("bg-color", s.Bg) addBool("bold", s.Bold) addBool("dim", s.Dim) addBool("italic", s.Italic) addBool("underlined", s.Underlined) addBool("blink", s.Blink) addBool("inverse", s.Inverse) if sb.Len() == 0 { return parse.Quote(s.Text) } return fmt.Sprintf("(styled-segment %s%s)", parse.Quote(s.Text), sb.String()) } // IterateKeys feeds the function with all valid attributes of styled-segment. func (*Segment) IterateKeys(fn func(v any) 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 any) (v any, ok bool) { switch k { case "text": v = s.Text case "fg-color": if s.Fg == nil { return "default", true } return s.Fg.String(), true case "bg-color": if s.Bg == nil { return "default", true } return s.Bg.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 any) (any, 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 any) (any, 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. Any // existing SGR state will be cleared. func (s *Segment) VTString() string { sgr := s.SGR() if sgr == "" { return "\033[m" + s.Text } return fmt.Sprintf("\033[;%sm%s\033[m", sgr, s.Text) } elvish-0.21.0/pkg/ui/text_segment_test.go000066400000000000000000000027531465720375400204020ustar00rootroot00000000000000package 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{Fg: Red, Bg: Blue}, "foo"}). Repr("(styled-segment foo &fg-color=red &bg-color=blue)"). Index("fg-color", "red"). Index("bg-color", "blue") } var textSegmentVTStringTests = []struct { name string seg *Segment wantVTString string }{ { name: "seg with no style", seg: &Segment{Text: "foo"}, wantVTString: "\033[mfoo", }, { name: "seg with style", seg: &Segment{Style: Style{Bold: true}, Text: "foo"}, wantVTString: "\033[;1mfoo\033[m", }, } func TestTextSegmentVTString(t *testing.T) { for _, tc := range textSegmentVTStringTests { t.Run(tc.name, func(t *testing.T) { if got := tc.seg.VTString(); got != tc.wantVTString { t.Errorf("VTString of %#v is %q, want %q", tc.seg, got, tc.wantVTString) } if got := tc.seg.String(); got != tc.wantVTString { t.Errorf("String of %#v is %q, want %q", tc.seg, got, tc.wantVTString) } }) } } elvish-0.21.0/pkg/ui/text_test.go000066400000000000000000000123041465720375400166510ustar00rootroot00000000000000package ui import ( "errors" "testing" "src.elv.sh/pkg/eval/vals" "src.elv.sh/pkg/tt" ) func TestT(t *testing.T) { tt.Test(t, T, Args("test").Rets(Text{&Segment{Text: "test"}}), Args("test red", FgRed).Rets(Text{&Segment{ Text: "test red", Style: Style{Fg: Red}}}), Args("test red", FgRed, Bold).Rets(Text{&Segment{ Text: "test red", Style: Style{Fg: Red, Bold: true}}}), ) } func TestConcat(t *testing.T) { tt.Test(t, Concat, Args().Rets(Text(nil)), Args(T("red", FgRed), T("blue", FgBlue), T("green", FgGreen)). Rets(Text{red("red"), blue("blue"), green("green")}), // Merging adjacent segments with the same style Args(T("red", FgRed), T("red", FgRed)).Rets(T("redred", FgRed)), // Concatenating texts with multiple segments Args(Concat(T("red", FgRed), T("blue", FgBlue)), Concat(T("blue", FgBlue), T("green", FgGreen))). Rets(Text{red("red"), blue("blueblue"), green("green")}), // Concatenating empty texts Args(T(""), T("red", FgRed), T("")).Rets(T("red", FgRed)), ) } func TestTextAsElvishValue(t *testing.T) { vals.TestValue(t, T("text")). Kind("ui:text"). Repr("[^styled text]"). AllKeys("0"). Index("0", &Segment{Text: "text"}). IndexError("a", errors.New("index must be integer")) vals.TestValue(t, Concat(T("red", FgRed), T("blue", FgBlue), T("green", FgGreen))). Index("0..2", Concat(T("red", FgRed), T("blue", FgBlue))) vals.TestValue(t, T("text", FgRed)). Repr("[^styled (styled-segment text &fg-color=red)]") vals.TestValue(t, T("text", Bold)). Repr("[^styled (styled-segment text &bold)]") } var ( text0 = Text{} text1 = Text{red("lorem")} text2 = Text{red("lorem"), blue("foobar")} ) var partitionTests = []*tt.Case{ 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).Named("Text.Partition"), partitionTests...) } func TestCountRune(t *testing.T) { text := Text{red("lorem"), blue("ipsum")} tt.Test(t, tt.Fn(Text.CountRune).Named("Text.CountRune"), 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).Named("Text.CountLines"), 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).Named("Text.SplitByRune"), 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")}, }), Args(Text{red("lorem\n")}, '\n').Rets( []Text{ {red("lorem")}, nil, }), ) } func TestTrimWcwidth(t *testing.T) { tt.Test(t, tt.Fn(Text.TrimWcwidth).Named("Text.TrimWcwidth"), 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) { t.Helper() for _, test := range tests { vtString := test.text.VTString() if vtString != test.wantVTString { t.Errorf("got %q, want %q", vtString, test.wantVTString) } } } func red(s string) *Segment { return &Segment{Style{Fg: Red}, s} } func blue(s string) *Segment { return &Segment{Style{Fg: Blue}, s} } func green(s string) *Segment { return &Segment{Style{Fg: Green}, s} } elvish-0.21.0/pkg/ui/ui.go000066400000000000000000000001301465720375400152350ustar00rootroot00000000000000// Package ui contains types that may be used by different editor frontends. package ui elvish-0.21.0/pkg/wcwidth/000077500000000000000000000000001465720375400153335ustar00rootroot00000000000000elvish-0.21.0/pkg/wcwidth/wcwidth.go000066400000000000000000000132101465720375400173300ustar00rootroot00000000000000// 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{} ) // Data in combiningRanges and the implementation of OfRune is based on // http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c, which is in the public domain. var combiningRanges = [][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 inRange(r rune, ranges [][2]rune) bool { n := len(ranges) i := sort.Search(n, func(i int) bool { return r <= ranges[i][1] }) return i < n && r >= ranges[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 inRange(r, combiningRanges) { 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.21.0/pkg/wcwidth/wcwidth_test.go000066400000000000000000000031301465720375400203670ustar00rootroot00000000000000package wcwidth import ( "testing" "src.elv.sh/pkg/tt" ) var Args = tt.Args func TestOf(t *testing.T) { tt.Test(t, Of, 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, Trim, 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, Force, // 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, TrimEachLine, Args("abcdefg\n你好", 3).Rets("abc\n你"), ) } elvish-0.21.0/tools/000077500000000000000000000000001465720375400142415ustar00rootroot00000000000000elvish-0.21.0/tools/buildall.elv000077500000000000000000000063751465720375400165570ustar00rootroot00000000000000#!/usr/bin/env elvish # This script is supposed to be run with Elvish at the same commit. Either # ensure that Elvish is built and installed from the repo, or use "go run # ./cmd/elvish tools/buildall.elv ...". use flag use os use platform use str var platforms = [ [&arch=amd64 &os=linux] [&arch=amd64 &os=darwin] [&arch=amd64 &os=freebsd] [&arch=amd64 &os=openbsd] [&arch=amd64 &os=netbsd] [&arch=amd64 &os=windows] [&arch=386 &os=linux] [&arch=386 &os=windows] [&arch=arm64 &os=linux] [&arch=arm64 &os=darwin] [&arch=riscv64 &os=linux] ] fn sha256sum-if-available {|name| if (has-external sha256sum) { sha256sum $name > $name.sha256sum } } var usage = ^ 'buildall.elv [-name name] [-variant variant] [-keep-bin] go-pkg dst-dir Builds $go-pkg, writing a binary $dst-dir/$GOOS-$GOARCH/$name and an archive for a predefined list of supported GOOS/GOARCH combinations. The binary is removed after the archive is created, unless -keep-bin is specified. For GOOS=windows, the binary name has an .exe suffix, and 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 value of $variant will be used to override src.elv.sh/pkg/buildinfo.BuildVariant. ' fn main {|go-pkg dst-dir &name=elvish &variant='' &keep-bin=$false| tmp E:CGO_ENABLED = 0 for platform $platforms { var arch os = $platform[arch] $platform[os] var bin-dir = $dst-dir/$os'-'$arch os:mkdir-all $bin-dir var bin-name-in-archive bin-name archive-name = ( if (eq $os windows) { put elvish.exe $name{.exe .zip} } else { put elvish $name{'' .tar.gz} }) print 'Building for '$os'-'$arch'... ' tmp E:GOOS E:GOARCH = $os $arch try { go build ^ -trimpath ^ -ldflags '-X src.elv.sh/pkg/buildinfo.BuildVariant='$variant ^ -o $bin-dir/$bin-name-in-archive ^ $go-pkg } catch e { echo 'Failed' continue } # This is needed to get files appear in the root of the archive files. tmp pwd = $bin-dir # Archive files store the modification timestamp of files. Change it to a # fixed point in time to make the archive files reproducible. touch -d 2000-01-01T00:00:00Z $bin-name-in-archive if (eq $os windows) { zip -q $archive-name $bin-name-in-archive } else { # If we create a .tar.gz file directly with the tar command, the # resulting archive will contain the timestamp of the .tar file, # rendering the result unreproducible. As a result, we need to do it in # two steps. tar cf $bin-name.tar $bin-name-in-archive touch -d 2022-01-01T00:00:00Z $bin-name.tar gzip -f $bin-name.tar } sha256sum-if-available $archive-name mv $bin-name-in-archive $bin-name # Update the modification time again to reflect the actual modification # time. (Technically this makes the file appear slightly newer han it really # is, but it's close enough). touch $bin-name sha256sum-if-available $bin-name if (not $keep-bin) { rm $bin-name } echo 'Done' } } flag:call $main~ $args &on-parse-error={|_| print $usage; exit 1} elvish-0.21.0/tools/check-disallowed.sh000077500000000000000000000026631465720375400200110ustar00rootroot00000000000000#!/bin/sh # Check Go source files for disallowed content. ret=0 # We have our own trimmed-down copy of net/rpc to reduce binary size. Make sure # that dependency on net/rpc is not accidentally introduced. x=$(find . -name '*.go' | xargs grep '"net/rpc"') if test "$x" != ""; then echo "======================================================================" echo 'Disallowed import of net/rpc:' echo "======================================================================" echo "$x" ret=1 fi # The correct functioning of the unix: module depends on some certain calls not # being made elsewhere. x=$(find . -name '*.go' | egrep -v '\./pkg/(mods/unix|daemon|testutil)' | xargs egrep 'unix\.(Umask|Getrlimit|Setrlimit)') if test "$x" != ""; then echo "======================================================================" echo 'Disallowed call of unix.{Umask,Getrlimit,Setrlimit}:' echo "======================================================================" echo "$x" ret=1 fi # doc:show depends on references to language.html to not have a ./ prefix. x=$(find . -name '*.elv' | xargs grep '(\.\/language\.html') if test "$x" != ""; then echo "======================================================================" echo 'Disallowed use of ./ in link destination to language.html' echo "======================================================================" echo "$x" ret=1 fi exit $ret elvish-0.21.0/tools/check-fmt-go.sh000077500000000000000000000005451465720375400170500ustar00rootroot00000000000000#!/bin/sh -e # Check if Go files are properly formatted without modifying them. echo 'Go files need these changes:' # The grep is needed because `goimports -d` and `gofmt -d` always exits with 0. 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.21.0/tools/check-fmt-md.sh000077500000000000000000000004221465720375400170350ustar00rootroot00000000000000#!/bin/sh -e # Check if Markdown files are properly formatted without modifying them. echo 'Markdown files that need changes:' if find . -name '*.md' | grep -v '/node_modules/' | xargs go run src.elv.sh/cmd/elvmdfmt -width 80 -d | grep .; then exit 1 fi echo ' None!' elvish-0.21.0/tools/check-gen.sh000077500000000000000000000016321465720375400164260ustar00rootroot00000000000000#!/bin/sh # Check that generated Go source files are up to date. git_unstaged() { # The output of "git status -s" starts with two letters XY, where Y is the # status in the working tree. Files that are staged in the index have Y # being a space; exclude them. git status -s | grep '^.[^ ]' } if ! which git >/dev/null; then echo "$0 requires Git" exit 1 fi if test "$(git_unstaged)" != ""; then echo "$0 must be run from a Git repo with no unstaged changes or untracked files" exit 1 fi go generate ./... || exit 1 x=$(git_unstaged) if test "$x" != ""; then echo "======================================================================" echo "Generated Go code is out of date. See" echo "https://github.com/elves/elvish/blob/master/CONTRIBUTING.md#generated-code" echo "======================================================================" echo "$x" exit 1 fi elvish-0.21.0/tools/imports-graph.elv000066400000000000000000000037201465720375400175470ustar00rootroot00000000000000use flag use re use str var prefix = src.elv.sh/ fn keep-if {|p| each {|x| if ($p $x) { put $x }} } fn get {|x k def| if (has-key $x $k) { put $x[$k] } else { put $def } } fn get-cluster {|x| put (re:find '^'(re:quote $prefix)'[^/]+/[^/]+' $x)[text] } fn node {|x| put '"'(str:trim-prefix $x $prefix)'"' } fn main {|&merge-clusters=$false| var imports-of = [&] var q = [$prefix''cmd/elvish] var seen = [&q[0]=$true] var clusters = [&] while (not-eq $q []) { var next-q = [] for pkg $q { var c = (get-cluster $pkg) set clusters[$c] = [(all (get $clusters $c [])) $pkg] var @imports = ( go list -json $pkg | all (get (from-json) Imports []) | keep-if {|x| str:has-prefix $x $prefix}) set imports-of[$pkg] = $imports var @new-pkgs = (all $imports | keep-if {|x| not (has-key $seen $x) set seen[$x] = $true }) set @next-q = (all $next-q) (all $new-pkgs) } set q = $next-q } echo 'strict digraph imports {' echo ' rankdir = LR;' echo ' node [shape = box, width = 1.5];' echo ' splines = ortho;' echo ' nodesep = 0.1;' if $merge-clusters { for pkg [(keys $imports-of)] { for import $imports-of[$pkg] { var src = (get-cluster $pkg) var dst = (get-cluster $import) if (not-eq $src $dst) { echo ' '(node $src)' -> '(node $dst)';' } } } } else { var cluster-seq = 0 for c [(keys $clusters)] { var pkgs = $clusters[$c] if (<= (count $pkgs) 1) { continue } echo ' subgraph cluster'$cluster-seq' {' echo ' style = filled;' echo ' color = lightgrey;' for pkg $clusters[$c] { echo ' '(node $pkg)';' } echo ' }' set cluster-seq = (+ $cluster-seq 1) } for pkg [(keys $imports-of)] { for import $imports-of[$pkg] { echo ' '(node $pkg)' -> '(node $import)';' } } } echo '}' } flag:call $main~ $args elvish-0.21.0/tools/pre-push000077500000000000000000000021761465720375400157400ustar00rootroot00000000000000#!/bin/sh -e # To use this script as a Git hook: # # cd .git/hooks # from repo root # ln -s ../../tools/pre-push . if ! git diff HEAD --quiet; then if git diff --cached --quiet; then echo 'Local changes exist and none is staged; stashing.' git stash trap 'r=$?; git stash pop; trap - EXIT; exit $r' EXIT INT HUP TERM else echo 'Local changes exist and some are staged; not stashing.' echo 'Make a commit, stash all the changes, or unstage all the changes.' exit 1 fi fi # go.work is needed for gopls to function correctly when working on Go code # inside website/ from VS Code (and possibly other development environments), # but it changes the behavior of "go test", so force disable it. export GOWORK=off make test all-checks make -C website check-rellinks # A quick cross compilation test. Not exhaustive, but will catch most issues. if test `go env GOOS` = windows; then GOOS=linux GOARCH=amd64 go build ./... GOOS=linux GOARCH=amd64 go test -o NUL -c ./... else GOOS=windows GOARCH=amd64 go build ./... GOOS=windows GOARCH=amd64 go test -o /dev/null -c ./... fi elvish-0.21.0/tools/prune-cover.sh000077500000000000000000000006471465720375400170540ustar00rootroot00000000000000#!/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.21.0/tools/run-race.elv000077500000000000000000000013071465720375400164710ustar00rootroot00000000000000#!/usr/bin/env elvish # Prints "-race" if running on a platform that supports the race detector. use re # Keep in sync with the official list here: # https://golang.org/doc/articles/race_detector#Requirements var supported-os-arch = [ linux/amd64 linux/ppc64le linux/arm64 linux/s390x freebsd/amd64 netbsd/amd64 darwin/amd64 darwin/arm64 ] if (eq 1 (go env CGO_ENABLED)) { var os arch = (go env GOOS GOARCH) var os-arch = $os/$arch if (has-value $supported-os-arch $os-arch) { echo -race } elif (eq windows/amd64 $os-arch) { # Race detector on windows/amd64 requires gcc: # https://github.com/golang/go/issues/27089 if (has-external gcc) { echo -race } } } elvish-0.21.0/vscode/000077500000000000000000000000001465720375400143645ustar00rootroot00000000000000elvish-0.21.0/vscode/.eslint.js000066400000000000000000000007741465720375400163060ustar00rootroot00000000000000/**@type {import('eslint').Linter.Config} */ // eslint-disable-next-line no-undef module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', ], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], rules: { 'semi': [2, "always"], '@typescript-eslint/no-unused-vars': 0, '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-non-null-assertion': 0, } }; elvish-0.21.0/vscode/.gitignore000066400000000000000000000000341465720375400163510ustar00rootroot00000000000000/dist /node_modules /*.vsix elvish-0.21.0/vscode/.vscodeignore000066400000000000000000000000201465720375400170440ustar00rootroot00000000000000node_modules/** elvish-0.21.0/vscode/HACKING.md000066400000000000000000000007251465720375400157560ustar00rootroot00000000000000# Developing the Visual Studio Code extension Follow these steps to run the extension from source: 1. Install NPM dependencies: `npm install`. 2. Open the repository root (**not** this directory) in VS Code, and choose "Run Extension" from the [debug side bar](https://code.visualstudio.com/docs/editor/debugging). See Visual Studio Code's documentation on [developing language extensions](https://code.visualstudio.com/api/language-extensions/overview). elvish-0.21.0/vscode/LICENSE000066400000000000000000000024241465720375400153730ustar00rootroot00000000000000Copyright (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.21.0/vscode/README.md000066400000000000000000000005111465720375400156400ustar00rootroot00000000000000# Elvish for Visual Studio Code This extension provides support for [Elvish](https://elv.sh) in [Visual Studio Code](https://code.visualstudio.com). Current functionalities: - Syntax highlighting and configuration (e.g. bracket pairs) - Error checking and basic autocompletion, using Elvish's builtin language server elvish-0.21.0/vscode/icon.png000066400000000000000000000215271465720375400160310ustar00rootroot00000000000000PNG  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.21.0/vscode/language-configuration.json000066400000000000000000000011621465720375400217070ustar00rootroot00000000000000{ "comments": { "lineComment": "#", }, // Barewords and variables have different patterns. This is the pattern for // variables because it's more important for VS Code to recognize variable // names properly. "wordPattern": "[\\w\\d_:~-]+", "brackets": [ ["{", "}"], ["[", "]"], ["(", ")"] ], "autoClosingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"] ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"] ] } elvish-0.21.0/vscode/package-lock.json000066400000000000000000002225551465720375400176130ustar00rootroot00000000000000{ "name": "elvish", "version": "0.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "elvish", "version": "0.3.5", "license": "BSD-2-Clause", "dependencies": { "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/node": "^22.2.0", "@types/vscode": "^1.65.0", "@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/parser": "^8.0.1", "esbuild": "^0.23.0", "eslint": "^9.8.0", "typescript": "^5.5.4", "typescript-formatter": "^7.2.2" }, "engines": { "vscode": "^1.65.0" } }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/@eslint/eslintrc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/@eslint/js": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "engines": { "node": ">=12.22" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/retry": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" }, "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" }, "engines": { "node": ">= 8" } }, "node_modules/@types/node": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.13.0" } }, "node_modules/@types/vscode": { "version": "1.92.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", "integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/type-utils": "8.0.1", "@typescript-eslint/utils": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@typescript-eslint/parser": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/types": "8.0.1", "@typescript-eslint/typescript-estree": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1", "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/type-utils": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.0.1", "@typescript-eslint/utils": "8.0.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@typescript-eslint/types": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/types": "8.0.1", "@typescript-eslint/typescript-estree": "8.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.0.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "node_modules/commandpost": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz", "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==", "dev": true }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "dependencies": { "path-type": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", "dev": true, "dependencies": { "commander": "^2.19.0", "lru-cache": "^4.1.5", "semver": "^5.6.0", "sigmund": "^1.0.1" }, "bin": { "editorconfig": "bin/editorconfig" } }, "node_modules/editorconfig/node_modules/lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "node_modules/editorconfig/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" } }, "node_modules/editorconfig/node_modules/yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "dev": true }, "node_modules/esbuild": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.23.0", "@esbuild/android-arm": "0.23.0", "@esbuild/android-arm64": "0.23.0", "@esbuild/android-x64": "0.23.0", "@esbuild/darwin-arm64": "0.23.0", "@esbuild/darwin-x64": "0.23.0", "@esbuild/freebsd-arm64": "0.23.0", "@esbuild/freebsd-x64": "0.23.0", "@esbuild/linux-arm": "0.23.0", "@esbuild/linux-arm64": "0.23.0", "@esbuild/linux-ia32": "0.23.0", "@esbuild/linux-loong64": "0.23.0", "@esbuild/linux-mips64el": "0.23.0", "@esbuild/linux-ppc64": "0.23.0", "@esbuild/linux-riscv64": "0.23.0", "@esbuild/linux-s390x": "0.23.0", "@esbuild/linux-x64": "0.23.0", "@esbuild/netbsd-x64": "0.23.0", "@esbuild/openbsd-arm64": "0.23.0", "@esbuild/openbsd-x64": "0.23.0", "@esbuild/sunos-x64": "0.23.0", "@esbuild/win32-arm64": "0.23.0", "@esbuild/win32-ia32": "0.23.0", "@esbuild/win32-x64": "0.23.0" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.2", "eslint-visitor-keys": "^4.0.0", "espree": "^10.1.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" } }, "node_modules/eslint-scope": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" }, "engines": { "node": ">=0.10" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" }, "engines": { "node": ">=8.6.0" } }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { "is-glob": "^4.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" }, "engines": { "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true, "license": "ISC" }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { "is-glob": "^4.0.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { "node": ">=0.8.19" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { "p-locate": "^5.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" } }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { "p-limit": "^3.0.2" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "dev": true }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", "dev": true }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, "engines": { "node": ">=8.0" } }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "license": "MIT", "engines": { "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" } }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=14.17" } }, "node_modules/typescript-formatter": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz", "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==", "dev": true, "dependencies": { "commandpost": "^1.0.0", "editorconfig": "^0.15.0" }, "bin": { "tsfmt": "bin/tsfmt" }, "engines": { "node": ">= 4.2.0" }, "peerDependencies": { "typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev" } }, "node_modules/undici-types": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", "dev": true, "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.5" }, "engines": { "vscode": "^1.82.0" } }, "node_modules/vscode-languageserver-protocol": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } } } } elvish-0.21.0/vscode/package.json000066400000000000000000000111101465720375400166440ustar00rootroot00000000000000{ "name": "elvish", "displayName": "Elvish", "description": "Elvish language support for Visual Studio Code", "version": "0.3.5", "publisher": "elves", "license": "BSD-2-Clause", "icon": "icon.png", "repository": { "type": "git", "url": "https://github.com/elves/elvish" }, "engines": { "vscode": "^1.65.0" }, "categories": [ "Programming Languages", "Snippets" ], "activationEvents": [], "main": "./dist/extension.js", "contributes": { "languages": [ { "id": "elvish", "aliases": [ "Elvish", "elvish" ], "extensions": [ ".elv" ], "firstLine": "^#!/.*\\belvish\\b$", "configuration": "./language-configuration.json" }, { "id": "elvish-transcript", "aliases": [ "Elvish transcript", "elvish transcript" ], "extensions": [ ".elvts" ], "configuration": "./transcript-language-configuration.json" }, { "id": "elvish-in-markdown", "aliases": [ "Elvish in Markdown" ] } ], "grammars": [ { "language": "elvish", "scopeName": "source.elvish", "path": "./syntaxes/elvish.tmLanguage.json" }, { "language": "elvish-transcript", "scopeName": "source.elvish-transcript", "path": "./syntaxes/elvish-transcript.tmLanguage.json", "embeddedLanguages": { "meta.embedded.block.elvish": "elvish" } }, { "language": "elvish-in-markdown", "scopeName": "source.elvish.in.markdown", "path": "./syntaxes/elvish-in-markdown.tmLanguage.json", "injectTo": [ "text.html.markdown" ], "embeddedLanguages": { "meta.embedded.block.elvish": "elvish", "meta.embedded.block.elvish-transcript": "elvish-transcript" } } ], "configuration": { "title": "Elvish", "properties": { "elvish.trace.server": { "type": "string", "enum": [ "off", "messages", "verbose" ], "default": "off", "description": "Trace communication between VS Code and the Elvish language server. Trace messages are shown in the Elvish Language Server output channel." } } }, "snippets": [ { "language": "elvish", "path": "./snippets/snippets.json" } ], "commands": [ { "command": "elvish.updateTranscriptOutputForCodeAtCursor", "title": "Elvish: update transcript output for code at cursor" } ], "menus": { "commandPalette": [ { "command": "elvish.updateTranscriptOutputForCodeAtCursor", "when": "editorLangId =~ /^elvish(-transcript)?$/" } ] }, "keybindings": [ { "command": "elvish.updateTranscriptOutputForCodeAtCursor", "key": "alt+enter", "when": "editorTextFocus && editorLangId =~ /^elvish(-transcript)?$/" } ] }, "scripts": { "vscode:prepublish": "npm run check && npm run build", "check": "tsc -p . --noEmit", "build": "esbuild ./src/extension.ts --bundle --outfile=./dist/extension.js --external:vscode --format=cjs --platform=node --minify --sourcemap", "watch": "npm run build -- --watch" }, "dependencies": { "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/node": "^22.2.0", "@types/vscode": "^1.65.0", "@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/parser": "^8.0.1", "esbuild": "^0.23.0", "eslint": "^9.8.0", "typescript": "^5.5.4", "typescript-formatter": "^7.2.2" } } elvish-0.21.0/vscode/sample.elv000066400000000000000000000013641465720375400163610ustar00rootroot00000000000000# A sample file to test syntax highlighting. nop "double \n quoted" and 'single '' quoted' # comment # Various variable contexts nop $pid var var-name = { var fn-name~ = {var not-var-name} } nop (set var-name = foo | tmp var-name = bar); del var-name with var-name = foo { } # This one doesn't work, we need a real parser or some very messy heuristics with [var-name1 = foo] [var-name2 = bar] { } for var-name [] { } try { } catch var-name { } # Builtin functions != a (nop b) | echo c # Builtin special command and a b # "operator" use re # "other" if a { } elif b { } else { } try { } except err { } else { } finally { } # Metacharacters echo ** () [] # Regression tests set-env # should highlight entire set-env set-foo # should highlight nothing elvish-0.21.0/vscode/sample.elvts000066400000000000000000000005221465720375400167230ustar00rootroot00000000000000# A sample file for elvish transcript files. # // The line above should be highlighted as a heading, and this line should be // highlighted as a comment. // H2s are also supported: ## This is an H2 ## // The code after the prompt should be highlighted as Elvish code. ~> if $true { echo ok } ok // The output should be unhighlighted. elvish-0.21.0/vscode/snippets/000077500000000000000000000000001465720375400162315ustar00rootroot00000000000000elvish-0.21.0/vscode/snippets/snippets.json000066400000000000000000000046761465720375400210060ustar00rootroot00000000000000{ "lambda (single line)": { "prefix": "lambda", "body": "{|${1:arguments}| ${2:body}}" }, "lambda (multi line)": { "prefix": "lambda", "body": [ "{|${1:arguments}|", "\t${2:body}", "}" ] }, "var command": { "prefix": "var", "body": "var ${1:names} = ${2:values}" }, "set command": { "prefix": "set", "body": "set ${1:names} = ${2:values}" }, "tmp command": { "prefix": "tmp", "body": "tmp ${1:names} = ${2:values}" }, "if command": { "prefix": "if", "body": [ "if ${1:condition} {", "\t${2:body}", "}" ] }, "if command with else": { "prefix": "if-else", "body": [ "if ${1:condition} {", "\t${2:body}", "} else {", "\t${3:body}", "}" ] }, "while command": { "prefix": "while", "body": [ "while ${1:condition} {", "\t${2:body}", "}" ] }, "while command with else": { "prefix": "while-else", "body": [ "while ${1:condition} {", "\t${2:body}", "} else {", "\t${3:body}", "}" ] }, "for command": { "prefix": "for", "body": [ "for ${1:variable} ${2:container} {", "\t${3:body}", "}" ] }, "for command with else": { "prefix": "for-else", "body": [ "for ${1:variable} ${2:container} {", "\t${3:body}", "} else {", "\t${4:body}", "}" ] }, "try command with catch": { "prefix": "try-catch", "body": [ "try {", "\t${1:body}", "} catch ${2:name} {", "\t${3:body}", "}" ] }, "try command with finally": { "prefix": "try-finally", "body": [ "try {", "\t${1:body}", "} finally {", "\t${2:body}", "}" ] }, "fn command": { "prefix": "fn", "body": [ "fn ${1:name} {|${1:arguments}|", "\t${2:body}", "}" ] }, "pragma command": { "prefix": "pragma", "body": "pragma ${1:name} = ${2:value}" } } elvish-0.21.0/vscode/src/000077500000000000000000000000001465720375400151535ustar00rootroot00000000000000elvish-0.21.0/vscode/src/extension.ts000066400000000000000000000062171465720375400175450ustar00rootroot00000000000000import * as path from 'path'; import * as child_process from 'child_process'; import * as vscode from 'vscode'; import { LanguageClient } from 'vscode-languageclient/node'; let client: LanguageClient | undefined; export function activate(context: vscode.ExtensionContext) { client = new LanguageClient( "elvish", "Elvish Language Server", { command: "elvish", args: ["-lsp"] }, { documentSelector: [{ scheme: "file", language: "elvish" }] } ); client.start(); context.subscriptions.push(vscode.commands.registerCommand( 'elvish.updateTranscriptOutputForCodeAtCursor', updateTranscriptOutputForCodeAtCursor)); } export function deactivate() { return client?.stop(); } interface UpdateInstruction { fromLine: number; toLine: number; content: string; } async function updateTranscriptOutputForCodeAtCursor() { const editor = vscode.window.activeTextEditor; if (!editor) { return; } const {dir, base} = path.parse(editor.document.uri.fsPath); // VS Code's line number is 0-based, but the ELVISH_TRANSCRIPT_RUN protocol // uses 1-based line numbers. This is also used in the UI, where the user // expects 1-based line numbers. const lineno = editor.selection.active.line + 1; await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: `Running ${base}:${lineno}...` }, async (progress, token) => { // Transcript tests uses what's on the disk, so we have to save the // document first. await editor.document.save(); // See godoc of pkg/eval/evaltest for the protocol. const {error, stdout} = await exec( "go test -run TestTranscripts", { cwd: dir, env: {...process.env, ELVISH_TRANSCRIPT_RUN: `${base}:${lineno}`}, }); if (error) { const match = stdout.match(/UPDATE (.*)$/m); if (match) { const {fromLine, toLine, content} = JSON.parse(match[1]) as UpdateInstruction; const range = new vscode.Range( new vscode.Position(fromLine-1, 0), new vscode.Position(toLine-1, 0)); editor.edit((editBuilder) => { editBuilder.replace(range, content); }); } else { vscode.window.showWarningMessage(`Unexpected test failure: ${stdout}`) } } else { // TODO: Distinguish two different cases: // // - Output is already up-to-date // - Cursor is in an invalid position. // // This needs to be detected by evaltest first. vscode.window.showInformationMessage('Nothing to do.') } }); } // Wraps child_process.exec to return a promise. function exec(cmd: string, options: child_process.ExecOptions): Promise<{error: child_process.ExecException|null, stdout: string, stderr: string}> { return new Promise((resolve) => { child_process.exec(cmd, options, (error, stdout, stderr) => { resolve({error, stdout, stderr}); }); }); } elvish-0.21.0/vscode/syntaxes/000077500000000000000000000000001465720375400162425ustar00rootroot00000000000000elvish-0.21.0/vscode/syntaxes/elvish-in-markdown.tmLanguage.json000066400000000000000000000033471465720375400247450ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "scopeName": "source.elvish.in.markdown", "injectionSelector": "L:text.html.markdown", "patterns": [ { "name": "markup.fenced_code.block.markdown", "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(elvish)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { "3": { "name": "punctuation.definition.markdown" }, "4": { "name": "fenced_code.block.language.markdown" }, "5": { "name": "fenced_code.block.language.attributes.markdown" } }, "endCaptures": { "3": { "name": "punctuation.definition.markdown" } }, "patterns": [ { "begin": "(^|\\G)(\\s*)(.*)", "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", "contentName": "meta.embedded.block.elvish", "patterns": [ { "include": "source.elvish" } ] } ] }, { "name": "markup.fenced_code.block.markdown", "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(elvish-transcript)((\\s+|:|,|\\{|\\?)[^`~]*)?$)", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { "3": { "name": "punctuation.definition.markdown" }, "4": { "name": "fenced_code.block.language.markdown" }, "5": { "name": "fenced_code.block.language.attributes.markdown" } }, "endCaptures": { "3": { "name": "punctuation.definition.markdown" } }, "patterns": [ { "begin": "(^|\\G)(\\s*)(.*)", "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", "contentName": "meta.embedded.block.elvish-transcript", "patterns": [ { "include": "source.elvish-transcript" } ] } ] } ] } elvish-0.21.0/vscode/syntaxes/elvish-transcript.tmLanguage.json000066400000000000000000000012361465720375400247030ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "Elvish transcript", "scopeName": "source.elvish-transcript", "fileTypes": [ "elvts" ], "patterns": [ { "begin": "(^|\\G)[~/][^ ]*> ", "while": "(^|\\G) ", "contentName": "meta.embedded.block.elvish", "patterns": [ { "include": "source.elvish" } ] }, { "name": "markup.heading.1.elvish-transcript", "match": "(^|\\G)# .* #$" }, { "name": "markup.heading.2.elvish-transcript", "match": "(^|\\G)## .* ##$" }, { "name": "comment.line.double-slash.elvish-transcript", "match": "(^|\\G)//.*$" } ] } elvish-0.21.0/vscode/syntaxes/elvish.tmLanguage.json000066400000000000000000000055351465720375400225220ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "Elvish", "scopeName": "source.elvish", "fileTypes": [ "elv" ], "patterns": [ { "name": "string.quoted.double.elvish", "begin": "\"", "end": "\"", "patterns": [ { "name": "constant.character.escape.elvish", "match": "\\\\." } ] }, { "name": "string.quoted.single.elvish", "begin": "'", "end": "'" }, { "name": "comment.line.number-sign.elvish", "begin": "#", "end": "$" }, { "name": "variable.other.elvish", "match": "\\$[\\w\\d_:~-]*" }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(var|set|tmp|with|del)((\\s+[\\w\\d_:~-]+)+)", "captures": { "1": { "name": "keyword.other.elvish" }, "2": { "name": "variable.other.elvish" } } }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(for)\\s+([\\w\\d_:~-]+)", "captures": { "1": { "name": "keyword.control.elvish" }, "2": { "name": "variable.other.elvish" } } }, { "match": "(?<=})\\s+(catch)\\s+([\\w\\d_:~-]+)", "captures": { "1": { "name": "keyword.control.elvish" }, "2": { "name": "variable.other.elvish" } } }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(nop|!=|!=s|%|\\*|\\+|-gc|-ifaddrs|-log|-override-wcwidth|-stack|-|/|<|<=|<=s||>=|>=s|>s|all|assoc|base|bool|break|call|cd|compare|constantly|continue|count|defer|deprecate|dissoc|drop|each|eawk|echo|eq|eval|exact-num|exec|exit|external|fail|fg|float64|from-json|from-lines|from-terminated|get-env|has-env|has-external|has-key|has-value|is|keys|kind-of|make-map|multi-error|nop|not-eq|not|ns|num|one|only-bytes|only-values|order|peach|pprint|print|printf|put|rand|randint|range|read-line|read-upto|repeat|repr|resolve|return|run-parallel|search-external|set-env|show|sleep|slurp|src|styled|styled-segment|take|tilde-abbr|time|to-json|to-lines|to-string|to-terminated|unset-env|use-mod|wcswidth)(?=[\\s)}<>;|&])", "captures": { "1": { "name": "support.function.elvish" } } }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(and|or|coalesce)(?=[\\s)}<>;|&])", "captures": { "1": { "name": "keyword.operator.elvish" } } }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(use|var|set|tmp|with|del|pragma|fn)(?=[\\s)}<>;|&])", "captures": { "1": { "name": "keyword.other.elvish" } } }, { "match": "(?<=\\G|^|\\{ |\\{\t|\\(|\\||\\;)\\s*(while|for|try|if)(?=[\\s)}<>;|&])", "captures": { "1": { "name": "keyword.control.elvish" } } }, { "match": "(?<=})\\s+(elif|else|catch|finally)(?=[\\s)}<>;|&])", "captures": { "1": { "name": "keyword.control.elvish" } } }, { "name": "keyword.operator.elvish", "match": "[*?|&;<>()\\[\\]{}]" } ] } elvish-0.21.0/vscode/transcript-language-configuration.json000066400000000000000000000002201465720375400240700ustar00rootroot00000000000000{ "comments": { "lineComment": "//" }, // See comment in language-configuration.json. "wordPattern": "[\\w\\d_:~-]+", } elvish-0.21.0/vscode/tsconfig.json000066400000000000000000000003371465720375400170760ustar00rootroot00000000000000{ "compilerOptions": { "module": "commonjs", "target": "es2020", "lib": ["es2020"], "outDir": "tsc-out", "sourceMap": true, "strict": true, "rootDir": "src" }, "exclude": ["node_modules", ".vscode-test"] } elvish-0.21.0/website/000077500000000000000000000000001465720375400145435ustar00rootroot00000000000000elvish-0.21.0/website/.gitignore000066400000000000000000000001311465720375400165260ustar00rootroot00000000000000*.html !/template.html !*-ttyshot.html *.raw /tools/*.bin /_* /Elvish.docset /Elvish.tgz elvish-0.21.0/website/Makefile000066400000000000000000000027141465720375400162070ustar00rootroot00000000000000DST_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) # Generates the website into $(DST_DIR). gen: tools/gensite.bin $(HTMLS) tools/gensite.bin . $(DST_DIR) ln -sf `pwd`/fonts `pwd`/favicons/* $(DST_DIR)/ # Generates docset into $(DOCSET_DST_DIR). docset: tools/gensite.bin $(HTMLS) ELVISH_DOCSET_MODE=1 tools/gensite.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 tools/*.bin $(HTMLS) $(DST_DIR) $(DOCSET_TMP_DIR) $(DOCSET_DST_DIR) ifdef TTYSHOT %-ttyshot.html: %-ttyshot.elvts tools/ttyshot.bin tools/ttyshot.bin $(if $(findstring verbose,$(TTYSHOT)),-v,) -o $@ $< else %-ttyshot.html: @: ttyshot generation disabled by default endif .PHONY: gen docset publish check-rellinks clean # Don't remove intermediate targets .SECONDARY: # Rules below have dynamic prerequisite lists, which requires GNU Make's # .SECONDEXPANSION. .SECONDEXPANSION: tools/%.bin: cmd/% $$(wildcard cmd/%/*) go.mod ../go.mod $$(shell tools/cmd-deps ./cmd/%) go build -o $@ ./$< %.html: %.md tools/md2html.bin $$(shell tools/md-deps $$@) tools/md2html.bin < $< > $@ elvish-0.21.0/website/README.md000066400000000000000000000110231465720375400160170ustar00rootroot00000000000000# Source for Elvish's website This directory contains source for Elvish's official website. The documents are written in [CommonMark](https://commonmark.org) 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 collection of static HTML files, built from Markdown files with a custom toolchain. You need the following software to build it: - Go, with the same version requirement as Elvish itself. - GNU Make (any "reasonably modern" version should do). 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. ### Additional workflows - Run `make check-rellinks` to ensure that relative links between pages are valid. - Run `make Elvish.docset` to build a docset containing all the reference docs. [Docset](https://kapeli.com/docsets) is a format for packaging documentation for offline consumption. Both workflows use a Python script under the hood, and require Python 3 and Beautiful Soup 4 (install with `pip install --user bs4`). ## Transcripts Documents can contain **transcripts** of Elvish sessions, identified by the language tag `elvish-transcript`. A simple example: ````markdown ```elvish-transcript ~> echo foo | str:to-upper (one) ▶ FOO ``` ```` When the website is built, the toolchain will highlight the `echo foo | str:to-upper (one)` part correctly as Elvish code. To be exact, the toolchain uses the following heuristic to determine the range of Elvish code: - It looks for what looks like a prompt, which starts with either `~` or `/`, ends with `>` and a space, with no spaces in between. - It then extends the range downwards, as long as the line starts with N whitespaces, where N is the length of the prompt (including the trailing space). As long as you use Elvish's default prompt, you should be able to rely on this heuristic. ## Ttyshots Some of the pages include "ttyshots" that show the content of Elvish sessions. They are HTML files with terminal attributes converted to CSS classes, generated from corresponding instruction files. By convention, the instruction files have names ending in `-ttyshot.elvts` (because they are syntactically Elvish transcripts), and the generated HTML files have names ending in `-ttyshot.html`. The generation process depends on [`tmux`](https://github.com/tmux/tmux) and a built `elvish` in `PATH`. Windows is not supported. ### Instruction syntax Ttyshot instruction files look like Elvish transcripts, with the following differences: - It should not contain the output of commands. Anything that is not part of an input at a prompt causes a parse error. - If the Elvish code starts with `#` followed immediately by a letter, it is treated instead as a command to sent to `tmux`. The most useful one (and only one being used now) is `send-keys`. For example, the following instructions runs `cd /tmp`, and sends Ctrl-N to trigger navigation mode at the next prompt: ```elvish-transcript ~> cd /tmp ~> #send-keys C-N ``` ### Generating ttyshots Unlike other generated website artifacts, generated ttyshots are committed into the repository, and the `Makefile` rule to generate them is disabled by default. This is because the process to generate ttyshots is relatively slow and may have network dependencies. To turn on ttyshot generation, pass `TTYSHOT=1` to `make` (where `1` can be replaced by any non-empty string). For example, to generate a single ttyshot, run `make TTYSHOT=1 foo-ttyshot.html`. To build the website with ttyshot generation enabled, run `make TTYSHOT=1`. The first time you generate ttyshots, `make` will build the `ttyshot` tool, and regenerate all ttyshots. Subsequent runs will only regenerate ttyshots whose instruction files have changed. ## 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.21.0/website/blog/000077500000000000000000000000001465720375400154665ustar00rootroot00000000000000elvish-0.21.0/website/blog/0.10-release-notes.md000066400000000000000000000151341465720375400211360ustar00rootroot00000000000000Version 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.21.0/website/blog/0.11-release-notes.md000066400000000000000000000137371465720375400211460ustar00rootroot00000000000000Version 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 reliably, 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.21.0/website/blog/0.12-release-notes.md000066400000000000000000000112231465720375400211330ustar00rootroot00000000000000Version 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.21.0/website/blog/0.13-release-notes.md000066400000000000000000000126371465720375400211460ustar00rootroot00000000000000Version 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.21.0/website/blog/0.13.1-release-notes.md000066400000000000000000000011701465720375400212730ustar00rootroot00000000000000Version 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.21.0/website/blog/0.14.0-release-notes.md000066400000000000000000000136501465720375400213010ustar00rootroot00000000000000Version 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.21.0/website/blog/0.14.1-release-notes.md000066400000000000000000000007151465720375400213000ustar00rootroot00000000000000Version 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.21.0/website/blog/0.15.0-release-notes.md000066400000000000000000000077471465720375400213140ustar00rootroot00000000000000Version 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.21.0/website/blog/0.16.0-release-notes.md000066400000000000000000000112221465720375400212740ustar00rootroot00000000000000Version 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.21.0/website/blog/0.17.0-release-notes.md000066400000000000000000000062661465720375400213110ustar00rootroot00000000000000Version 0.17.0 has been released on 2021-12-10, 4 months after 0.16.0, bringing new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # 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. - The `range` command now supports counting down. 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.21.0/website/blog/0.18.0-release-notes.md000066400000000000000000000034461465720375400213070ustar00rootroot00000000000000Version 0.18.0 has been released on 2022-03-20, 4 months after 0.17.0, bringing new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # Breaking changes - All features deprecated in 0.17.0 have been removed. - Within double quotes, `\x` followed by two hexadecimal digits and `\` followed by three octal digits now encode an individual byte, rather than a codepoint. - Using a lone `try` without `except` or `finally` is now forbidden; this does not do anything useful and is almost certainly an incorrect attempt to suppress an exception. # Deprecated features Deprecated features will be removed in 0.19.0. The following deprecated features trigger a warning whenever the code is parsed or compiled, even if it is not executed: - The legacy temporary assignment syntax (e.g. `a=foo echo $a`) is deprecated. Use the new `tmp` command instead (e.g. `tmp a = foo; echo $a`). - The clause to catch exceptions in the `try` special command is now spelt with `catch`; the old keyword `except` is deprecated. # Notable bugfixes - The output longer than terminal width is now shown fully on Windows Terminal. - Changing directories in the navigation mode now correctly runs hooks and updates `$E:PWD`. # Notable new features - Elvish now ships a builtin language server that can be started with `elvish -lsp`. - A new `flag:` module for parsing command-line flags. - A new `tmp` special command for doing temporary assignments. - A new `defer` command to schedule a function to be run when the current closure finishes execution. - A new `call` command to call a command, using a list for and a map for options. - A new `$unix:rlimits` variable allows manipulation of resource limits. elvish-0.21.0/website/blog/0.19.1-release-notes.md000066400000000000000000000100431465720375400213000ustar00rootroot00000000000000Elvish 0.19.1 has been released on 2023-03-05, almost a year after 0.18.0, bringing new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. **Note**: The 0.19.0 version was tagged prematurely by mistake, but it has been picked up by some package managers. As a result, the 0.19.0 version is considered to be "skipped" officially. If your package manager provides a 0.19.0 version, it is probably identical to 0.19.1 in functionalities. **Note 2**: The commit tagged 0.19.1 would advertise itself as 0.19.0 when built. To fix this inconsistency, 0.19.2 was tagged with the correct version information. # Notable new features - A new `doc` module provides access to the documentation of builtin modules. - A new `conj` command "appends" values to a list, and has a guaranteed time complexity independent of the size of the list. - A new `inexact-num` converts its argument to an inexact number. It is functionally identical to the now deprecated `float64` command since the Go float64 type is the only underlying inexact number type for now. Its behavior may change in future if there are more underlying types for inexact numbers. - A new type of interactive abbreviation: `edit:command-abbr` ([#1472](https://b.elv.sh/1472)). - The `order` and `compare` commands now support boolean values ([#1585](https://b.elv.sh/1585)). - A new `path:join` command and `path:separator` and `path:list-separator` variables ([#1562](https://b.elv.sh/1562)). - A new `runtime:` module that contains paths important for the Elvish runtime ([#1385](https://b.elv.sh/1385), [#1423](https://b.elv.sh/1423)). - A new `compact` command that replaces consecutive runs of equal values with a single copy, similar to the Unix `uniq` command. - The `order` command has a new `&key` option ([#1570](https://b.elv.sh/1570)). - A new `benchmark` command has been added ([#1586](https://b.elv.sh/1586)). - When checking compilation errors, Elvish no longer stops after the first error found. For example, if `$a` and `$b` are both not defined, `echo $a $b` now yields two errors. This applies to both the interactive REPL and `elvish -compile-only`. - When using an unimported builtin modules from the REPL, the REPL now shows the `use` command needed to import it, which can be executed from a key binding. This functionality is bound to Ctrl-A by default. - New variables exposing the terminal and null device in an OS-agnostic fashion: `$path:dev-tty` and `$path:dev-null`. They are `/dev/tty` and `/dev/null` on Unix, and `CON` and `NUL` on Windows ([#1633](https://b.elv.sh/1633)). # Breaking changes - When a `styled` or `styled-segment` is printed to terminal, the resulting sequence will now always ignore any existing SGR state. - Symbolic links are now always treated as ordinary files by the global modifiers `type:dir` and `type:regular` in wildcard expansions. - Support for shared vars has been removed, along with its API (`store:shared-var`, `store:set-shared-var` and `store:del-shared-var`). - The `try` command no longer supports the `except` keyword. It has been superseded by the `catch` keyword. # Deprecated features Deprecated features will be removed in 0.20.0. The following deprecated features trigger a warning whenever the code is parsed and compiled, even if it is not executed: - The `float64` command is now deprecated. Use `num` for constructing a typed number, or `inexact-num` for constructing an inexact number. The documentation has advertised it as deprecated since the 0.16.0 release, but deprecation warnings were never added. # Notable bugfixes - Temporary assignment on an unset environment variables no longer leave it set to an empty string ([#1448](https://b.elv.sh/1448)). - Broken symbolic links no longer terminate a wildcard expansion prematurely ([#1240](https://b.elv.sh/1240)). - On Windows, command completion for executables now also works for local files. elvish-0.21.0/website/blog/0.20.0-release-notes.md000066400000000000000000000041501465720375400212710ustar00rootroot00000000000000Elvish 0.20.0 has been released on 2024-02-11, 11 months after 0.19.1, bringing new features and bugfixes. As usual, [prebuilt binaries](https://elv.sh/get) are offered for most common platforms. # Notable new features - A new `os:` module providing access to operating system functionality. - A new `read-bytes` command for reading a fixed number of bytes. - New commands in the `file:` module: `file:open-output`, `file:seek` and `file:tell`. - Maps now have their keys sorted when printed. - The `peach` command now has a `&num-workers` option ([#648](https://github.com/elves/elvish/issues/648)). - The `from-json` command now supports integers of arbitrary precision, and outputs them as exact integers rather than inexact floats. - A new `str:fields` command ([#1689](https://b.elv.sh/1689)). - The `order` and `compare` commands now support a `&total` option, which allows sorting and comparing values of mixed types. - The language server now supports showing the documentation of builtin functions and variables on hover ([#1684](https://b.elv.sh/1684)). - Elvish now respects the [`NO_COLOR`](https://no-color.org) environment variable. Builtin UI elements as well as styled texts will not have colors if it is set and non-empty. # Notable bugfixes - `has-value $li $v` now works correctly when `$li` is a list and `$v` is a composite value, like a map or a list. - A bug with how the hash code of a map was computed could lead to unexpected results when using maps as map keys; it has now been fixed. # Breaking changes - The `except` keyword in the `try` command was deprecated since 0.18.0 and is now removed. Use `catch` instead. - The `float64` command was deprecated since 0.16.0 and emitted deprecation warnings since 0.19.1, and is now removed. Use `num` or `inexact-num` instead. # Deprecated features Deprecated features will be removed in 0.21.0. The following deprecated features trigger a warning whenever the code is parsed and compiled, even if it is not executed: - The `eawk` command is now deprecated. Use `re:awk` instead. elvish-0.21.0/website/blog/0.20.1-release-notes.md000066400000000000000000000002711465720375400212720ustar00rootroot00000000000000Elvish 0.20.1 fixes a test that is failing on s370x. There are no user-visible changes. For changes since the 0.19.x series, see the [0.20.0 release notes](0.20.0-release-notes.html). elvish-0.21.0/website/blog/0.9-release-notes.md000066400000000000000000000072321465720375400210660ustar00rootroot00000000000000Version 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.21.0/website/blog/index.toml000066400000000000000000000036341465720375400175000ustar00rootroot00000000000000prelude = "prelude" autoIndex = true [[articles]] name = "0.20.1-release-notes" title = "Elvish 0.20.1 release notes" timestamp = "2024-02-14" [[articles]] name = "0.20.0-release-notes" title = "Elvish 0.20.0 release notes" timestamp = "2024-02-11" [[articles]] name = "0.19.1-release-notes" title = "Elvish 0.19.1 release notes" timestamp = "2023-03-05" [[articles]] name = "0.18.0-release-notes" title = "Elvish 0.18.0 release notes" timestamp = "2022-03-20" [[articles]] name = "0.17.0-release-notes" title = "Elvish 0.17.0 release notes" timestamp = "2021-12-10" [[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.21.0/website/blog/live.md000066400000000000000000000030301465720375400167430ustar00rootroot00000000000000After 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.21.0/website/blog/newsletter-july-2017.md000066400000000000000000000124531465720375400215610ustar00rootroot00000000000000Welcome 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.21.0/website/blog/newsletter-sep-2017.md000066400000000000000000000061761465720375400213720ustar00rootroot00000000000000Welcome 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.21.0/website/blog/prelude.md000066400000000000000000000002211465720375400174430ustar00rootroot00000000000000The draft release notes for the next release can be found [here on GitHub](https://github.com/elves/elvish/blob/master/0.21.0-release-notes.md). elvish-0.21.0/website/cmd/000077500000000000000000000000001465720375400153065ustar00rootroot00000000000000elvish-0.21.0/website/cmd/gensite/000077500000000000000000000000001465720375400167445ustar00rootroot00000000000000elvish-0.21.0/website/cmd/gensite/.gitignore000066400000000000000000000005141465720375400207340ustar00rootroot00000000000000# 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.21.0/website/cmd/gensite/feed_tmpl.go000066400000000000000000000013641465720375400212360ustar00rootroot00000000000000package main const feedTemplText = ` {{ .SiteTitle }} {{ .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.21.0/website/cmd/gensite/main.go000066400000000000000000000105551465720375400202250ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "path/filepath" "sort" "time" ) func main() { args := os.Args[1:] if len(args) != 2 { log.Fatal("Usage: gensite ") } 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 site configuration. conf := &siteConf{} 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 // 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, groups []group) { // 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, groups, css, js} executeToFile(categoryTmpl, cd, filepath.Join(catDir, "index.html")) } for _, cat := range conf.Categories { 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 groups []group if catConf.AutoIndex { groups = makeGroups(catConf.Articles, catConf.Groups) } renderCategoryIndex(cat.Name, prelude, css, js, groups) // 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)) recents.insert(a) } } // 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) } } func makeGroups(articles []articleMeta, groupMetas []groupMeta) []group { groups := make(map[int]*group) for _, am := range articles { g := groups[am.Group] if g == nil { g = &group{} if 0 <= am.Group && am.Group < len(groupMetas) { g.groupMeta = groupMetas[am.Group] } groups[am.Group] = g } g.Articles = append(g.Articles, am) } indices := make([]int, 0, len(groups)) for i := range groups { indices = append(indices, i) } sort.Ints(indices) sortedGroups := make([]group, len(groups)) for i, idx := range indices { sortedGroups[i] = *groups[idx] } return sortedGroups } elvish-0.21.0/website/cmd/gensite/model.go000066400000000000000000000062241465720375400203770ustar00rootroot00000000000000package main import ( "html/template" "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 site. // siteConf represents the global site configuration. type siteConf 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 // site configuration. type categoryMeta struct { Name string Title string NavHTML template.HTML } // categoryConf represents the configuration of a category. Note that the // metadata is found in the global site configuration and not duplicated here. type categoryConf struct { Prelude string AutoIndex bool ExtraCSS []string ExtraJS []string Articles []articleMeta Groups []groupMeta } // articleMeta represents the metadata of an article, found in a category // configuration. type articleMeta struct { Name string Title string Note string Group int 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 } // Metadata of a group, found in a category index.toml. type groupMeta struct { Intro string } // All information about a group to render it. type group struct { groupMeta Articles []articleMeta } 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 } // decodeTOML decodes the named file in TOML into a pointer. func decodeTOML(fname string, v any) { _, 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.21.0/website/cmd/gensite/render.go000066400000000000000000000046011465720375400205530ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "text/template" "time" ) // This file contains functions and types for rendering the site. // baseDot is the base for all "dot" structures used as the environment of the // HTML template. type baseDot struct { SiteTitle string Author string RootURL string HomepageTitle string Categories []categoryMeta CategoryMap map[string]string BaseCSS string } func newBaseDot(bc *siteConf, 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 Groups []group 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]any{ "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 any, 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.21.0/website/cmd/md2html/000077500000000000000000000000001465720375400166555ustar00rootroot00000000000000elvish-0.21.0/website/cmd/md2html/elvdoc.go000066400000000000000000000045201465720375400204610ustar00rootroot00000000000000package main import ( "fmt" "html" "io" "net/url" "os" "sort" "strings" "src.elv.sh/pkg/elvdoc" ) func writeElvdocSections(w io.Writer, docs elvdoc.Docs) { writeSection := func(heading, entryType string, entries []elvdoc.Entry) { fmt.Fprintf(w, "# %s\n", heading) sort.Slice(entries, func(i, j int) bool { return symbolForSort(entries[i].Name) < symbolForSort(entries[j].Name) }) for _, entry := range entries { fmt.Fprintln(w) // 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(entry.Name) { fmt.Fprintf(w, "\n\n", entryType, url.QueryEscape(html.UnescapeString(s))) } // Convert directives into header attributes // (https://pandoc.org/MANUAL.html#extension-header_attributes), // which will get interpreted by [htmlCodec.Do]. var attrs []string for _, directive := range entry.Directives { if htmlID, ok := strings.CutPrefix(directive, "doc:html-id "); ok { attrs = append(attrs, "#"+strings.TrimSpace(htmlID)) } else if addedIn, ok := strings.CutPrefix(directive, "doc:added-in "); ok { attrs = append(attrs, "added-in="+addedIn) } else if strings.HasPrefix(directive, "doc:") { fmt.Fprintf(os.Stderr, "\033[31mWarning: unknown directive: %s\033[m\n", directive) } } attrString := "" if len(attrs) > 0 { attrString = " {" + strings.Join(attrs, " ") + "}" } // Print the header. fmt.Fprintf(w, "## %s%s\n\n", entry.Name, attrString) // Print the body - it's is guaranteed to have a trailing newline, // hence Fprint instead of Fprintln. fmt.Fprint(w, entry.FullContent()) } } if len(docs.Vars) > 0 { writeSection("Variables", "Variable", docs.Vars) } if len(docs.Fns) > 0 { if len(docs.Vars) > 0 { fmt.Fprintln(w) fmt.Fprintln(w) } writeSection("Functions", "Function", docs.Fns) } } func symbolForSort(s string) string { // Hack to sort unstable symbols close to their stable counterparts: for // example, let "-gc" appear between "gb" and "gd", but after "gc". if strings.HasPrefix(s, "-") { return s[1:] + "-" } return s } elvish-0.21.0/website/cmd/md2html/elvdoc_targets.go000066400000000000000000000016141465720375400222130ustar00rootroot00000000000000package main import ( "strings" "src.elv.sh/pkg/md" ) // Adds the implicit destination for [`foo`](). func addImplicitElvdocTargets(module string, ops []md.InlineOp) { for i := range ops { if i+2 < len(ops) && ops[i].Type == md.OpLinkStart && ops[i].Dest == "" && ops[i+1].Type == md.OpCodeSpan && ops[i+2].Type == md.OpLinkEnd { dest := elvdocTarget(ops[i+1].Text, module) ops[i].Dest, ops[i+2].Dest = dest, dest } } } // foo -> builtin.html#foo // $foo -> builtin.html#$foo // mod:foo -> mod.html#mod:foo // $mod:foo -> mod.html#$mod:foo func elvdocTarget(symbol, currentModule string) string { var module string i := strings.IndexRune(symbol, ':') if i == -1 { module = "builtin" } else if strings.HasPrefix(symbol, "$") { module = symbol[1:i] } else { module = symbol[:i] } if module == currentModule { return "#" + symbol } return module + ".html#" + symbol } elvish-0.21.0/website/cmd/md2html/elvdoc_test.elvts000066400000000000000000000022731465720375400222530ustar00rootroot00000000000000//each:elvdoc-to-md-in-global ////////////////// # no doc comment # ////////////////// ~> elvdoc-to-md '# not doc comment' ////////// # fn doc # ////////// ~> elvdoc-to-md ' # B. fn b { } # A. fn a { } ' # Functions ## a ```elvish a ``` A. ## b ```elvish b ``` B. ////////////////////// # fn doc and var doc # ////////////////////// ~> elvdoc-to-md ' # A. fn a { } # B. var b ' # Variables ## $b B. # Functions ## a ```elvish a ``` A. /////////////// # doc:html-id # /////////////// ~> elvdoc-to-md ' #doc:html-id add # Add. fn + { } ' # Functions ## + {#add} ```elvish + ``` Add. //////////////// # doc:added-in # //////////////// ~> elvdoc-to-md ' #doc:added-in 0.42 # Add. fn + { } ' # Functions ## + {added-in=0.42} ```elvish + ``` Add. elvish-0.21.0/website/cmd/md2html/highlight.go000066400000000000000000000036161465720375400211610ustar00rootroot00000000000000package main import ( "go/scanner" "go/token" "strings" "src.elv.sh/pkg/diag" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/ui" ) // Augments elvdoc.HighlightCodeBlock with additional syntax highlighting that // we don't want to be part of the Elvish binary. This currently just includes // Go; sh/bash is another candidate. func highlightCodeBlock(info, code string) ui.Text { if language, _, _ := strings.Cut(info, " "); language == "go" { return highlightGo(code) } return elvdoc.HighlightCodeBlock(info, code) } func highlightGo(code string) ui.Text { lexer, posBase := lexGo(code) var regions []ui.StylingRegion for { pos, tok, lit := lexer.Scan() if tok == token.EOF { break } if styling := styleGoToken(tok); styling != nil { from := int(pos) - posBase // Note that lit is "" for all operator tokens like "{" and "+". We // don't currently highlight them, but if we do we should use // tok.String() instead of lit for them. to := from + len(lit) region := ui.StylingRegion{ Ranging: diag.Ranging{From: from, To: to}, Styling: styling, } regions = append(regions, region) } } return ui.StyleRegions(code, regions) } // We don't use the full parser here, both because the scanner is sufficient for // highlighting, and we often highlight snippets of Go that are not necessarily // suitable for either go/parser.ParseFile or go/parser.ParseExpr. func lexGo(code string) (lexer scanner.Scanner, posBase int) { fset := token.NewFileSet() file := fset.AddFile("main.go", -1, len(code)) lexer.Init(file, []byte(code), nil, scanner.ScanComments) return lexer, file.Base() } func styleGoToken(tok token.Token) ui.Styling { switch tok { case token.ILLEGAL: return ui.Stylings(ui.FgBrightWhite, ui.BgRed) case token.COMMENT: return ui.FgCyan case token.CHAR, token.STRING: return ui.FgYellow default: if tok.IsKeyword() { return ui.FgBlue } return nil } } elvish-0.21.0/website/cmd/md2html/html_codec.go000066400000000000000000000173461465720375400213200ustar00rootroot00000000000000package main import ( "fmt" "html" "os" "regexp" "strings" "src.elv.sh/pkg/md" "src.elv.sh/pkg/strutil" "src.elv.sh/pkg/ui" ) // A wrapper of [md.HTMLCodec] implementing generic additional features. type htmlCodec struct { md.HTMLCodec preprocessInline func([]md.InlineOp) // Extensions numberSections, toc bool // Components of the current section number. Populated if numberSections or // toc is true (used for maintaining the sections array in the latter case). sectionNumbers []int // Tree of sections to be used in the table of content. Populated if toc is // true. The root node is a dummy node. sectionRoot section } type section struct { title string id string children []section } var ( numberSectionsRegexp = regexp.MustCompile(`\bnumber-sections\b`) tocRegexp = regexp.MustCompile(`\btoc\b`) ) func (c *htmlCodec) Do(op md.Op) { c.preprocessInline(op.Content) switch op.Type { case md.OpHeading: var id, addedIn string // These attributes are written by [writeElvdocSections]. for _, attr := range strings.Fields(op.Info) { if value, ok := strings.CutPrefix(attr, "#"); ok { id = value } else if value, ok := strings.CutPrefix(attr, "added-in="); ok { addedIn = value } } if id == "" { // Generate an ID using the inline text content converted to lower // case. id = strings.ToLower(plainTextOfInlineContent(op.Content)) } idHTML := html.EscapeString(processHTMLID(id)) level := op.Number // An empty onclick handler is needed for :hover to work on mobile: // https://stackoverflow.com/a/25673064/566659 fmt.Fprintf(c, ``, level, idHTML) // Render the content separately first; this may be used in the ToC too. var sb strings.Builder md.RenderInlineContentToHTML(&sb, op.Content) titleHTML := sb.String() // Number the section. if c.numberSections || c.toc { if level < len(c.sectionNumbers) { // When going from a higher section level to a lower one, // discard higher-level numbers. Discard higher-level section // numbers. For example, when going from a #### to a #, only // keep the first section number. c.sectionNumbers = c.sectionNumbers[:level] } if level == len(c.sectionNumbers) { c.sectionNumbers[level-1]++ } else { // We are going from a lower section level to a higher one (e.g. // # to ##), possibly with missing levels (e.g. # to ###). // Populate all with 1. for level > len(c.sectionNumbers) { c.sectionNumbers = append(c.sectionNumbers, 1) } } if c.numberSections { titleHTML = sectionNumberPrefix(c.sectionNumbers) + titleHTML } if c.toc { // The section numbers identify a path in the section tree. p := &c.sectionRoot for _, num := range c.sectionNumbers { idx := num - 1 if idx == len(p.children) { p.children = append(p.children, section{}) } p = &p.children[idx] } p.id = idHTML p.title = titleHTML } } c.WriteString(titleHTML) // Add self link fmt.Fprintf(c, ``, idHTML) if addedIn != "" { fmt.Fprintf(c, `added in %s`, addedIn) } fmt.Fprintf(c, "\n", op.Number) case md.OpHTMLBlock: if c.Len() == 0 && strings.HasPrefix(op.Lines[0], ") // // - Implicit links to elvdoc targets when link destination is empty and link // text is a single code span - for example, [`put`]() has destination // builtin.html#put (or just #put within doc for the builtin module itself) // // - Section numbers for headings (optional, turn on with <-- number-sections // -->) // // - Syntax highlighting of code blocks with language elvish or // elvish-transcript // // - Headers for code blocks when a code fence has additional text after the // language tag (like foo.elv in "```elvish foo.elv") // // - The "ttyshot" language tag in a fence code block causes the content to // be treated as a reference to a ttyshot and expanded. // // The comment block for optional features should appear before the main text, // and can contain multiple features (like ). // // A note on the implicit elvdoc target feature: ideally, we would like to use // Markdown's shortcut link feature and let a simple [`put`] have an implicit // target of builtin.html#put. Doing this has two prerequisites: // // - The [src.elv.sh/pkg/md] package must be modified to support shortcut // links. // // - The [src.elv.sh/pkg/elvdoc] package must be modified to add declarations // of these shortcut targets. This is because shortcut links in Markdown // must be declared, otherwise [`put`] is just literal [put]. // // This is feasible for targets in the same file, but much more tricky for // targets in a different module. For example, if the elvdoc of a:foo // references b:bar, we need to insert a declaration for b:bar to the elvdoc // of a:foo. // // Hence we've settled on using an empty target for now. It is a bit ugly but // hopefully not too ugly. package main import ( "os" "strings" "src.elv.sh/pkg/md" ) func main() { var expanded strings.Builder f := filterer{} f.filter(os.Stdin, &expanded) codec := &htmlCodec{} codec.preprocessInline = func(ops []md.InlineOp) { addImplicitElvdocTargets(f.module, ops) } md.Render(expanded.String(), md.SmartPunctsCodec{Inner: codec}) os.Stdout.WriteString(codec.String()) } elvish-0.21.0/website/cmd/md2html/transcripts_test.go000066400000000000000000000012271465720375400226210ustar00rootroot00000000000000package main import ( "embed" "strings" "testing" "src.elv.sh/pkg/elvdoc" "src.elv.sh/pkg/eval" "src.elv.sh/pkg/eval/evaltest" ) //go:embed *.elvts var transcripts embed.FS func TestTranscripts(t *testing.T) { evaltest.TestTranscriptsInFS(t, transcripts, "elvdoc-to-md-in-global", func(ev *eval.Evaler) { ev.ExtendGlobal(eval.BuildNs().AddGoFn("elvdoc-to-md", elvdocToMd)) }, ) } func elvdocToMd(fm *eval.Frame, src string) error { docs, err := elvdoc.Extract(strings.NewReader(src), "") if err != nil { return err } var sb strings.Builder writeElvdocSections(&sb, docs) _, err = fm.ByteOutput().WriteString(sb.String()) return err } elvish-0.21.0/website/cmd/runefreq/000077500000000000000000000000001465720375400171355ustar00rootroot00000000000000elvish-0.21.0/website/cmd/runefreq/main.go000066400000000000000000000007241465720375400204130ustar00rootroot00000000000000package 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.21.0/website/cmd/ttyshot/000077500000000000000000000000001465720375400170245ustar00rootroot00000000000000elvish-0.21.0/website/cmd/ttyshot/apply_dir.go000066400000000000000000000017661465720375400213500ustar00rootroot00000000000000package main import ( "fmt" "os" "path/filepath" ) // TODO: Add unit test and move this file to pkg/fsutil. // 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]any // File describes a file to create. type File struct { Perm os.FileMode Content string } // ApplyDir creates a given filesystem layout. func ApplyDir(dir Dir, root string) error { for name, file := range dir { path := filepath.Join(root, name) var err error switch file := file.(type) { case string: err = os.WriteFile(path, []byte(file), 0644) case File: err = os.WriteFile(path, []byte(file.Content), file.Perm) case Dir: err = os.MkdirAll(path, 0755) if err == nil { err = ApplyDir(file, path) } default: panic(fmt.Sprintf("file must be string, Dir, or File, got %T", file)) } if err != nil { return err } } return nil } elvish-0.21.0/website/cmd/ttyshot/interp.go000066400000000000000000000207511465720375400206610ustar00rootroot00000000000000//go:build unix package main import ( "bytes" _ "embed" "fmt" "io" "log" "os" "os/exec" "path/filepath" "regexp" "strings" "syscall" "time" "github.com/creack/pty" "src.elv.sh/pkg/sys/eunix" "src.elv.sh/pkg/ui" ) const ( cutMarker = "[CUT]" promptMarker = "[PROMPT]" ) // "tmux capture-pane" can save superfluous trailing spaces, so when removing // these patterns we need to account for that. var ( cutPattern = regexp.MustCompile(regexp.QuoteMeta(cutMarker) + " *\n") promptPattern = regexp.MustCompile(regexp.QuoteMeta(promptMarker) + " *\n") ) //go:embed rc.elv var rcElv string // Creates a temporary home directory for running tmux and elvish in. The caller // is responsible for removing the directory. func setupHome() (string, error) { homePath, err := os.MkdirTemp("", "ttyshot-*") if err != nil { return "", fmt.Errorf("create temp home: %w", err) } // The temporary directory may include symlinks in the path. Expand them so // that commands like tilde-abbr behaves as expected. resolvedHomePath, err := filepath.EvalSymlinks(homePath) if err != nil { return homePath, fmt.Errorf("resolve symlinks in homePath: %w", err) } homePath = resolvedHomePath err = ApplyDir(Dir{ // Directories to be used in navigation mode. "bash": Dir{}, "elvish": Dir{ "1.0-release.md": "1.0 has not been released yet.", "CONTRIBUTING.md": "", "Dockerfile": "", "LICENSE": "", "Makefile": "", "PACKAGING.md": "", "README.md": "", "SECURITY.md": "", "cmd": Dir{}, "go.mod": "", "go.sum": "", "pkg": Dir{}, "syntaxes": Dir{}, "tools": Dir{}, "vscode": Dir{}, "website": Dir{}, }, "zsh": Dir{}, // Will keep tmux and elvish's sockets, and raw output of capture-pane ".tmp": Dir{}, ".config": Dir{ "elvish": Dir{ "rc.elv": rcElv, }, }, }, homePath) return homePath, err } func createTtyshot(homePath string, script *script, saveRaw string) ([]byte, error) { ctrl, tty, err := pty.Open() if err != nil { return nil, err } defer ctrl.Close() defer tty.Close() winsize := pty.Winsize{Rows: script.rows, Cols: script.cols} pty.Setsize(ctrl, &winsize) rawPath := filepath.Join(homePath, ".tmp", "ttyshot.raw") if saveRaw != "" { saveRaw, err := filepath.Abs(saveRaw) if err != nil { return nil, fmt.Errorf("resolve path to raw dump file: %w", err) } os.Symlink(saveRaw, rawPath) } doneCh, err := spawnElvish(homePath, tty) if err != nil { return nil, err } finalEmptyPrompt := executeScript(script.ops, ctrl, homePath) log.Println("executed script, waiting for tmux to exit") // Drain outputs from the terminal. This is needed so that tmux can exit // properly without blocking on flushing outputs. go io.Copy(io.Discard, ctrl) err = <-doneCh if err != nil { return nil, err } rawBytes, err := os.ReadFile(rawPath) if err != nil { return nil, err } ttyshot := string(rawBytes) // Remove all content before the last cutMarker. segments := cutPattern.Split(ttyshot, -1) ttyshot = segments[len(segments)-1] // Strip all the prompt markers and the final empty prompt. segments = promptPattern.Split(ttyshot, -1) if finalEmptyPrompt { segments = segments[:len(segments)-1] } ttyshot = strings.Join(segments, "") ttyshot = strings.TrimRight(ttyshot, "\n") return []byte(sgrTextToHTML(ttyshot) + "\n"), nil } func spawnElvish(homePath string, tty *os.File) (<-chan error, error) { elvishPath, err := exec.LookPath("elvish") if err != nil { return nil, fmt.Errorf("find elvish: %w", err) } tmuxPath, err := exec.LookPath("tmux") if err != nil { return nil, fmt.Errorf("find tmux: %w", err) } tmuxSock := filepath.Join(homePath, ".tmp", "tmux.sock") elvSock := filepath.Join(homePath, ".tmp", "elv.sock") // Start tmux and have it start a hermetic Elvish session. tmuxCmd := exec.Cmd{ Path: tmuxPath, Args: []string{ tmuxPath, "-S", tmuxSock, "-f", "/dev/null", "-u", "-T", "256,RGB", "new-session", elvishPath, "-sock", elvSock}, Dir: homePath, Env: []string{ "HOME=" + homePath, "PATH=" + os.Getenv("PATH"), // The actual value doesn't matter here, as long as it can be looked // up in terminfo. We rely on the -T flag above to force tmux to // support certain terminal features. "TERM=xterm", }, Stdin: tty, Stdout: tty, Stderr: tty, } log.Println("started tmux, socket", tmuxSock) doneCh := make(chan error, 1) go func() { doneCh <- tmuxCmd.Run() log.Println("tmux exited") }() return doneCh, nil } func executeScript(script []op, ctrl *os.File, homePath string) (finalEmptyPrompt bool) { for _, op := range script { log.Println("waiting for prompt") err := waitForPrompt(ctrl) if err != nil { // TODO: Handle the error properly panic(err) } log.Println("executing", op) if op.isTmux { tmuxSock := filepath.Join(homePath, ".tmp", "tmux.sock") tmuxCmd := exec.Command("tmux", append([]string{"-S", tmuxSock}, strings.Fields(op.code)...)...) tmuxCmd.Env = []string{} err := tmuxCmd.Run() if err != nil { // TODO: Handle the error properly panic(err) } } else { for i, line := range strings.Split(op.code, "\n") { if i > 0 { // Use Alt-Enter to avoid committing the code ctrl.WriteString("\033\r") } ctrl.WriteString(line) } ctrl.WriteString("\r") } } if len(script) > 0 && !script[len(script)-1].isTmux { log.Println("waiting for final empty prompt") finalEmptyPrompt = true err := waitForPrompt(ctrl) if err != nil { // TODO: Handle the error properly panic(err) } } log.Println("sending Alt-q") // Alt-q is bound to a function that captures the content of the pane and // exits ctrl.Write([]byte{'\033', 'q'}) return finalEmptyPrompt } func waitForPrompt(f *os.File) error { return waitForOutput(f, promptMarker, func(bs []byte) bool { return bytes.HasSuffix(bs, []byte(promptMarker)) }) } func waitForOutput(f *os.File, expected string, matcher func([]byte) bool) error { var buf bytes.Buffer // It shouldn't take more than a couple of seconds to see the expected // output, so use a timeout an order of magnitude longer to allow for // overloaded systems. deadline := time.Now().Add(30 * time.Second) for { budget := time.Until(deadline) if budget <= 0 { break } ready, err := eunix.WaitForRead(budget, f) if err != nil { if err == syscall.EINTR { continue } return fmt.Errorf("waiting for tmux output: %w", err) } if !ready[0] { break } _, err = io.CopyN(&buf, f, 1) if err != nil { return fmt.Errorf("reading tmux output: %w", err) } if matcher(buf.Bytes()) { return nil } } return fmt.Errorf("timed out waiting for %s in tmux output; output so far: %q", expected, buf) } // We use this instead of html.EscapeString, since the latter also escapes ' and // " unnecessarily. var htmlEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">") func sgrTextToHTML(ttyshot string) string { t := ui.ParseSGREscapedText(ttyshot) var sb strings.Builder for i, line := range t.SplitByRune('\n') { if i > 0 { sb.WriteRune('\n') } for j, seg := range line { style := seg.Style var classes []string if style.Inverse { // The inverse attribute means that the foreground and // background colors should be swapped, which cannot be // expressed in pure CSS. To work around this, this code swaps // the foreground and background colors, and uses two special // CSS classes to indicate that the foreground/background should // take the inverse of the default color. style.Inverse = false style.Fg, style.Bg = style.Bg, style.Fg if style.Fg == nil { classes = append(classes, "sgr-7fg") } if style.Bg == nil { classes = append(classes, "sgr-7bg") } } for _, c := range style.SGRValues() { classes = append(classes, "sgr-"+c) } text := seg.Text // We pass -N to tmux capture-pane in order to correctly preserve // trailing spaces that have background colors. However, this // preserves unstyled trailing spaces too, which makes the ttyshot // harder to copy-paste, so strip it. if len(classes) == 0 && j == len(line)-1 { text = strings.TrimRight(text, " ") } if text == "" { continue } escapedText := htmlEscaper.Replace(text) if len(classes) == 0 { sb.WriteString(escapedText) } else { fmt.Fprintf(&sb, `%s`, strings.Join(classes, " "), escapedText) } } } return sb.String() } elvish-0.21.0/website/cmd/ttyshot/main.go000066400000000000000000000027671465720375400203130ustar00rootroot00000000000000//go:build unix // Command ttyshot generates a ttyshot HTML image from a ttyshot specification. // // See documentation in http://src.elv.sh/website#ttyshots. package main import ( "flag" "fmt" "io" "log" "os" ) func main() { err := run(os.Args[1:]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(args []string) error { fs := flag.NewFlagSet("ttyshot", flag.ExitOnError) outFlag := fs.String("o", "", "output file (defaults to spec-path + .html)") verboseFlag := fs.Bool("v", false, "enable verbose logging") saveRawFlag := fs.String("save-raw", "", "if non-empty, save output of capture-pane to this file") fs.Usage = func() { fmt.Fprintln(fs.Output(), "Usage: ttyshot [flags] spec-path") fs.PrintDefaults() } fs.Parse(args) if len(fs.Args()) != 1 { fs.Usage() os.Exit(1) } if !*verboseFlag { log.SetOutput(io.Discard) } specPath := fs.Args()[0] content, err := os.ReadFile(specPath) if err != nil { return err } script, err := parseScript(specPath, content) if err != nil { return err } homePath, err := setupHome() if err != nil { return fmt.Errorf("set up temporary home: %w", err) } defer func() { err := os.RemoveAll(homePath) if err != nil { fmt.Fprintln(os.Stderr, "Warning: unable to remove temp HOME:", err) } }() out, err := createTtyshot(homePath, script, *saveRawFlag) if err != nil { return err } outPath := *outFlag if outPath == "" { outPath = specPath + ".html" } return os.WriteFile(outPath, out, 0o644) } elvish-0.21.0/website/cmd/ttyshot/parse.go000066400000000000000000000031351465720375400204670ustar00rootroot00000000000000//go:build unix package main import ( "bytes" "fmt" "regexp" "strconv" "strings" "src.elv.sh/pkg/transcript" ) const ( defaultRows = 100 defaultCols = 52 ) type script struct { rows uint16 cols uint16 ops []op } type op struct { code string isTmux bool } var tmuxPattern = regexp.MustCompile(`^#[a-z]`) func parseScript(name string, content []byte) (*script, error) { n, err := transcript.Parse(name, bytes.NewReader(content)) if err != nil { return nil, err } if len(n.Children) > 0 { return nil, fmt.Errorf("%s:%d: subsections not supported in ttyshot specs", name, n.Children[0].LineFrom) } s := script{rows: defaultRows, cols: defaultCols} for _, directive := range n.Directives { name, rest, _ := strings.Cut(directive, " ") switch name { case "rows": rows, err := strconv.ParseUint(rest, 0, 16) if err != nil { return nil, fmt.Errorf("parse rows %q: %w", rest, err) } s.rows = uint16(rows) case "cols": cols, err := strconv.ParseUint(rest, 0, 16) if err != nil { return nil, fmt.Errorf("parse cols %q: %w", rest, err) } s.cols = uint16(cols) default: return nil, fmt.Errorf("unknown directive %q in directive line %q", name, directive) } } for _, interaction := range n.Interactions { if interaction.Output != "" { return nil, fmt.Errorf("%s:%d: output not supported in ttyshot specs", name, interaction.OutputLineFrom) } if tmuxPattern.MatchString(interaction.Code) { s.ops = append(s.ops, op{code: interaction.Code[1:], isTmux: true}) } else { s.ops = append(s.ops, op{code: interaction.Code}) } } return &s, nil } elvish-0.21.0/website/cmd/ttyshot/rc.elv000066400000000000000000000034461465720375400201470ustar00rootroot00000000000000# This is the interactive configuration for generating Elvish "ttyshots". { use store # Populate the interactive location history. store:add-dir ~ store:add-dir ~/tmp store:add-dir ~/bash store:add-dir ~/zsh store:add-dir /tmp store:add-dir /usr store:add-dir /usr/local/bin store:add-dir /usr/local/share store:add-dir /usr/local store:add-dir /opt store:add-dir ~/elvish/pkg/eval store:add-dir ~/elvish/pkg/edit store:add-dir ~/.config/elvish store:add-dir ~/elvish/website store:add-dir ~/.local/share/elvish store:add-dir ~/elvish # Populate the interactive command history. set @_ = ( store:add-cmd 'randint 1 10' store:add-cmd 'echo (styled warning: red) bumpy road' store:add-cmd 'echo "hello\nbye" > /tmp/x' store:add-cmd 'from-lines < /tmp/x' store:add-cmd 'cd /tmp' store:add-cmd 'cd ~/elvish' store:add-cmd 'git branch' store:add-cmd 'git checkout .' store:add-cmd 'git commit' range 10 | each {|_| store:add-cmd 'git status' } store:add-cmd 'cd /usr/local/bin' store:add-cmd 'echo $pwd' store:add-cmd '* (+ 3 4) (- 100 94)' range 9 | each {|_| store:add-cmd 'make' } store:add-cmd 'math:min 3 1 30' ) } # use store # Sync the history we just manufactured with this elvish process. edit:history:fast-forward set edit:global-binding[Alt-q] = { tmux capture-pane -epN > ~/.tmp/ttyshot.raw exit } set edit:max-height = 15 set edit:navigation:width-ratio = [8 18 30] set edit:before-readline = [$@edit:before-readline { echo '[PROMPT]' }] set edit:rprompt = (constantly (styled 'elf@host' inverse)) # These functions are used in some of the ttyshot scripts to ensure consistent # output that doesn't leak info about the machine used to create the ttyshot. fn whoami { echo elf } fn hostname { echo host.example.com } elvish-0.21.0/website/favicons/000077500000000000000000000000001465720375400163535ustar00rootroot00000000000000elvish-0.21.0/website/favicons/android-chrome-192x192.png000066400000000000000000000122071465720375400227130ustar00rootroot00000000000000PNG  IHDRRlgAMA a cHRMz&u0`:pQ<bKGD pHYsmhtIME +DLIDATxkl\e~/}qH'!!@҅ @El*+WU "RmE*.elAb+hZVK#r\䲸!!q0_Vuwkg3ߘ>=Q WuБx83ř(ap{tkfV ءBh N0 p[9K)|@p@ /2_.c "3"\Y p,FakK# ѻdBXC^u)P> hU] !Y}]+jрեBR(UD6$-U$$$$$$$$$$$$$d زl[fvp(LpL(}]%QX_Ha]!yey>|`<纇(K0R-ٲe /26m"+kmٿ?* o/(5TWSZNRrU*1P }H~bQio֭[Q^yx㍘=PGIK 6[2s'&> \HXv-`ɒ% weϞ=LLW՞gFZvPd1zN?~QEH@[[ǎ$ڵp8|5-[cSY+)2`K~K?D$lEtwөСC貾Vv;}yy<#4VYblTWS=|_[ik455_| Z)t͸T&t 2(R]c>}*++ Ƈ~xc=cC _:43g͞5>~u  / &|-p_3rU}k~"%7$1'8wn닄#tGgy4PX^}8u!~u׍7ɯ(Ir)Ou[=zTǃ< @L.vڅϧO#Gw^sTq2m;Ν;Jul߾oXYʟi6j6ۏ*Ә:mܸq#Ow~?cǎR3?aEҷS]btvv~zy.]Zwޡ^zɐ1>VUi-ϧ۷FMM ѣG9x E|E9[ܢ_kt`;x"/^T]Y p=(-@i#=?\LQ$:3Csh^Y媋$:;gg Xl$: Bv"$ ՠ%.I >67k<$dKH5hqO+\2Gt!:fqd0c}c!!0#8q~$2C5(+7 gSu1LK`.s%AE47 kR]rKsXY$0H .mR  M[Tu9t.dfs )jq3I PlŘ].)6[9y\&ЙAU{b̩|eL=@g[Mn٨9҉@O6h|1HAw* ]߹Ή=/-2@Gf.+7 =9n@'v=mn6I5[4<ԀݑU F,s^@'f*ی3HtPPD:%'FO#( @4F#nKFtVE-Mω, HZo$jHXr٨&HB= w$ȖeKX:@Q}psbO;F$w:-[+HA ݱt5HmMt&wZ|bZs ^6:ynxpW?FNQӫ/^$qrv8u{}M$~>1iKu[ۃAջAk^HS̋^8 (i.Q]ġ&l2O1kIbeӷtyv{U%Ĩjuz='z?sLbhbv|ñ3pq@@͆{j(1/*94 *0e;!s[$DgLtLɶ 5 9NhITGCUg9*-3*@VnM[Rx`PjH#9E9)^"8h!S<$Afh֔m/{p[ ^h|1%MS|þw43+ZlrrX{UJ=NT9fC Ovִe٨YohӞgܟ`W'pYkkQ&$8rPPDJNrKr9\eŞWG݃u)L - z!W^5E**h+]jAfTfא',OiY7d%Kk&<N4NgʙN*VUPZhd]ջMQ(oRdǒ>{ƫz9O3>p[U)^?EgB h Q<_Ԝ`ty8sw'>6(^Rd|~+WW>z_d|B*<3\5ջ33d|~ld Go>_~zb"PV?.#N~]%P`L拤Ά:5>E3tW 7?IǟvHm\5x-y~@K/|L͆ھFغ*! P7c(Mj̧^z?bUۚTcu=3 SLc?eg @Ddp090}?0 z7  uEB @@G쿖Q =N*WWRRO u18`a;ٲl )-l;99v;HH(B8&07?gbpۋ5IHe|3 KKKKKKKK an~ 0#BGtQB HJe Ru)PS>Q] !9iK"W]!R3{F~ե"÷@ BNTJg.g@}!7 >86.zxꇙ:Ku)0:`bꅙݡǁ@ 1 <͌c{`'ʇB|c"}N` ?~< z"g"^&쏈֛"ȒF8;DU,IJ;;A{A`#XC7R'DT']Y"d.nkgIGD}FWϹշ'^z6+f(BK"9A %ph'Τ?y,mq%tEXtdate:create2024-08-09T00:08:43+00:00":%tEXtdate:modify2024-08-09T00:08:43+00:00S tEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/android-chrome-512x512.png000066400000000000000000000346571465720375400227200ustar00rootroot00000000000000PNG  IHDRxgAMA a cHRMz&u0`:pQ<bKGD pHYsodtIME +DL8IDATxyus{kKZ = 8`_fK3gm/3l3MnE- !՚J珛JRIj}[U[Y򟨤$LR%jg K*;X@p%%MHH:2AI- tE )BJ:WR`_I)_^?$L[(/:I7Iz%՛IH7IT΁gxTKzHNRt fHIzT%HҰ@gNI0 I/&pcJz?t0,I_OO4t 7I*鳒L^eI)?\` @DG%gIͦ`}dpt8EI!CF$bIOIzPRa?_HҟHkeX+l?So:ow V~{̩p3[|x!Iݟ!K=IUv??!7@tɦm*Ut`S(~I b@a4J+ A YW< (AMx/ A1| ضT^ػ!io(_z3Fҿ?NP~z` &?+M fAn_$}3 K:9W+飄ʯ W o]F\a>!IܦM҃/' gǹ6ZIӐ'>z6~VJ=p.w *:3=l%]l9{IZl>*.xͅ>|@' Q~x;w?^"ix;1I$5N[Ro8k`u;$2,2;W?~q` xGN~C/(6̤F_q;)tW\4=3_9P'W_?$xfq$8כ!>I) 7}q$t"+ Fi#N(LX+'$)<[RRyHy@PD%l@+CNN xCLZ;4u!ISGՆ$UNUTl:pTqH @pD-Iu&%)g:p@@Q D (@@Q D (@@Q D (@@Q D (@@Q D (.gY±(|$b:g҈JjJ)VSqubU3:HqDVRQiћc-§geR(9Tr4hBLioBJ$L9%)g:Fեs=WVYYѨ; СCڽ{ۧL&c:kԗ\[Uޖ҆R==~ a{Tƕc @444kզMt5hŊD~'LW^?7z'4::ji9X5+kT}Nʗз)dZÿϗ#h`-S%%%}H7|.gL&#OS%IO"U-RjUSժXVa:c&z'B϶~&R#(0 ϴϴyfq?Çu}駿`jAuTsn*U YcB6U߶>|zQz*m:Dcc^bttTw/k||JKU^ 7z5 DF͗-=ʦ#X DžB!}WUՙs=zMG9A"Mo^-cC_{:f:yxXggz!]z饦kppX2^ު-[6ɦ:G{t# G]??t:t驧rsFˣjMnXpL ;l: xeYks=I:w߭߾IRӆ&uڥM\Mr:z qC᰾ojͦ _BA?f2[;ykJJM?E$jO_aBD"wݦ,3̢?NqMnҊs^SGT(fKteYu]w(7nT*ғO>/.E/҆/lPÚܳ.NHK]e|yKp/)\N7oַ9O8;W*Rmw.9ֿ٪mGp;C?M(T*kF[l9c/׺OSrӱQ(92Q\K۶mSee(wYFCCC}LMZ,VIB`vpXˋ$-]T7Xe^٪uU9u5 k}cMǰ[n9g*;+6t p(.ب/~c8k_g} ʗگ\zJ$ _LpO|bַeS%;c(.֦}Cc83JJNz`u_^H1K='P\sѨjllG>S^MU-҆o`' TTTF|SeS?10 ^٪ >xQ\;Tii0UW]uc?pZl55 |4U?1`u\,Dssj1;5,1ӱ`H2uZg:[O*]MMZ6hL|6m2^ꏯVI-Fpv5לxH0,j2 t WXG5 .e2 s9 V7_>ɲ,cUV\i:kX+V)b% j\h:hii1UNzut$s¡[ސOx~ӑ5֨nU/P\ ^НR-+n_lw:!kkU<3($ LMM*ns𱃦c%\n:y3Ufz~ӱ"t( x('1vhLGvyxTM<-6U:4xjϯ-IO590ɁI%JK*(H+JѰ"%Le}xiK]COZw65٬;=qH[6LkGutQ#;4xj9#5oRUmjجH1|i"%ҦdIʙtXL ܿOwO_rƏIhwxa@;5L}汪ο|u֩PQ5'KՁ0$~mqD"^{Mw(رo}zGWTc:!u?խ=ö^ONh?lמW36%P?N8u8\6mצmRhB__#FLGxϸ/O5iZפHiDI Kt7;M0 _)=ۧڡN}If8Va5,]S&1èL=\ƽWv79VO~_a}eY1 f7+5K| ,j\<J ']c^QO~f®]sϙQp{~GSG8n-xE&''o~t #򕯘`l:^ޚQ\_ǃu}t >۲Yic:L<2ַe:oFdt ی+cP\{Uoox饗a9n`~(.4::{t r9}W:]z1Wt 3(.G5Vx 113cc%5o`(.t]wu[\zWOt Gq~ w[OLL=y&''MGqTV /WQt (.cd~P;w4qGvQb]M(R%Mcie+0e>O;fJUuUNkLVO8x?yL0a5op `.!?$I^{(dtwMG1nr`R+X)+̾v G|iPF\ú II MgUa-qQ|-5R߳\A 8׾5]wuv8端.L?яLGq- ح ΆQO<֬Y氀r9=Zn^z%q\gK٬ɖYP111{W˖-=i{G?Quuu_z )گJeMeceH_ ú|@~JK wJА~l٢\oyM%u%خ?k:Jb˴i&]{Zjjkkڱc{1=cڶm'0O_wkw1WTmm9)+T8^{MR]EoftVIQ׉39-[h/+( [HHMt7LG\I!DFM=V~*Gp2UleW@EJ"jt u(Adži!1|aTޭ حNB y]VR[զcB ~}Xc'.[<DXhʎJ5s80JOrZ6rF\ ʪo[<-%Xh p=eNVR&1W.M#c< "=[nM4)f)a5EFj{t c0Z.oeq: W\]`.sxa%Gc!(9=ϮvcWBָ1S658AF\(5˃c2 R=[[.E|i&z'4t cAE\kI!x(~XX k8C\lh琎=f:+ X.Ss h.<Ūb9t QW&1X .>6pe ʗ|Ic(4i4!8AB\Dכ@P븩CV%jN=V5Ŧc.#eYj@ bu11a  ((n:B4mT8b:`; RXXmW8XX k9G\6ŋL$6BPZv2ec +/{JK԰ЦD+=t Vol7PX .znzoe.`Za:` 2LsgE±\t GQi.Rsn (X˸ .QRW5wW%XNk)p pKZv(TRF1.PNmw+Q踩tAeͲ _(aڮj3g-:1-v < `XM#`sk#ʚT^i:P0%yFF+lc?4oRIm5* @ aYRͦSA VDxTFV(7+T??/jX ~n:(TR%McFVsn*;YOeP\ڢP6a-vXxV#8VEQ1P (X)k,Sz1P L愧Q.?.8,n: y2 u*m,5VZ›(:na/YRFt7Q*cj[~<xYMg׫(^d:0oTlֿp8بzU,06c5؈мÁ9|6)61"_. `eo[p,l:0ؤfnI!5֨t 8sx A-#6PD%cK(@-ݴT҈0JeMecsB L 8@ R5֘y+:EQ1cP$ vUA @8Ӛ74+TďWߡ@~ /RQ cgD  QE EBjt LwrybU112%%^Qm:pZ`8ӲaX҆R5afǮp3 t 1ЋUvVr8\,~cp9V`64t7cCL=­(y}Qt ߫^ P()Vzo2z`T=[zL=+dy})(tܡP|~eQ|a&XRMS,؁1eo:5oR8685MX\$)~5E](hl9=g:]PXet M9ժZ^e:Fw~B/p GOK:lgMgMG= "-nuLMGK|3E`ɵKTTZd:FAMN=[[8VÚ1p pLɔ#3>癞YW pFeGjϫ54p6Z6Ȳ8P_$yߞg[*8PYca-~731DZ-3X (,ڮnS45{v`!McpI`~:g$rnKUt 8Ir]Pg:-z@b9S8Pt%prv&ѱcs~| JOM= 0C(R~\n {תt 6ߞ68~eyy`N1pޭen  p\y[/76}s`HBGv16*R1C븥×MO.znXX k9΢Boh7V -pi@RUsߴHQ3/k-(=Yj2DjUT{NQxeejXׅLag+ D@us:?mA%F^<8@–ݰt -9=gi^%@p zyJjKLǰB'03X Ph~7mw$>eSYON JJոt G@z*L?߫9Á .]`Y/p ؏@–oj7D`38v(KURijpJÿ6|q]"{QH~>d? Ը&S`SR_Mc8fbpw$ SX Q87wf$MѽG5uxZd} Q(e`65XuN]E+;t ҴIc8@0 ;Q(A7͎0}@ɴ{؉()VӆL6u ʪo-WE{) "OMlMhȦ@G~",f翙윭߻W ڍa؅@hZפxKt qZr,լQI}0v({2;&@@W\]:ynO1;P{7n4;$ig\cL?MkX@(`TDpXRۃy_@za5BE!5]%ְAMĞ V( 7wrDǎ3T}Җe|7b1^STVф'˩9v[QHu8CouܡPQpŧLIî`ܟ5˲oW&1}/Ya |q}c wؐs驴~7`)^YS*;+MǀOPK]28' ĮNiv;gcamYnpzZ%Mjt pi/ ԭӊwlGkŪbZcx/tVOv}"u獖G܁p,ig׫tOq{Pm_Ȳ,:5i׿Rz2m ԼYnXhXo 8 \y+9!)? 058:tv%sqKv`"-!NO[8. Jj馥>t$J(.zhrYF݄]Xxs\ꘊURSʎJ/-byRc)MNX\(\+U/Zœ%ܿ pZ.Ӟꏭ6Xv2 ˰lQZ_/7~-p-WdR! آt@4=֮vD#p lQt@Z{pLp04]d:f #NXZtty˦z_2h\h:feMv>ԑGLǀ*;*)f[P`F8-9|u>g,Uv1?-(Eq9w$Ȯ#z퇯U/g/n'K{k l^7j:lTUe:"%9D'˦z?\MGqFq~(tOWl I;S8d:lPR_ome)'=4oteY*gIf&|T&W7Tʘ6HOMGԤ $MNh_nQ&1`PPp3|50}#wSiU\i_y@-LGMM߳}z??s2@o|wGD0 lУw?lU,v`q%5R.=zwk(X ;[wSOxΓi=}݁*?^4vht Hb&l1CpRgګ]&wGx^Cq6;l:BL;#z篳ՅI?_ԯ5l1udJ'!~_8ɴ6u?խwVy[HMgTb$a:NBm^8$wf{Ol~D7W8tV?pQ`WvU@8Q6?]?w߾\ht,KMƿ=?ޣɁIqp؆>00DJ;ء=?٣Z[T/2wFh/{E6CIdғ[8SSz遗v}ʚY1 (tVQYZ.oђi}e:LN~Я#X5 U,Md:Ƃ&^79>.},lt9p8M6U&Ԝ~S:cMNJu~[c]mJJ|i6am[Lk=ק4ǹ~En}E-LM>Vz*'5R.{_|S)t9)9_(?95}z2\&L2L2+dj5]Ҥ ^Qcl鬆kG5{HG_;e,;}ڷ./fyy2ˀX!KKUZTzE)q)esx{5ƈviuP`,DʚT\xKKK:@r(LMe37矤'fJ&9~MrqǂP @@Q D (@@Q D (@@Q D (@($)k:pT6$)e:pT"$)i:pT2$QBL:tt #!ISG I:d:pSG Izt }!IMeI*4.)j: ]BR|z+` $Of: p [?c: pS[ii#$ >I SJjTVP+rU@t$NpDң[< I?43_NzcI%S"it'N qN-}+Yd)6iN\g%i@A|eWZypH+5,!IR'!twJ`QY.HwK0IHJ1%oM W:_:I KzNKITRtd$}T9>3\)~6`N%it!i^TgY:=0<8%iD;L?3pZ\8;ӏ?0)~/깹@%mo:#s}L騤4 ǥ%OKs0ӡz?wc 7~$Ssi@*$=%B!I yIjW~F_Է0I''tq_$snUHawJ3EF־P9'['_%58Er/_*|I¡/ ~c{ A 5prЫ2,K/_H$/ ~3IWH/d+JHzHsiI)OŽ9 _fwP}5pg%Wp%ɹ )|yY J?4'51.oI*QSIN;[L/$}C9vIwKb*C&rI",Ce/0S_Ij5:()?nҾpSV$]>|C^T~;rɅ L딿+p2ac/_ңOsinIoS.n+I?u5j$(MRB#o}^,'_a WHZ#|IQS{OJzFnӡ`6EQTR?EoDTl:,c$ߥvI?~ה?7 J/\%tEXtdate:create2024-08-09T00:08:43+00:00":%tEXtdate:modify2024-08-09T00:08:43+00:00S tEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/apple-touch-icon.png000066400000000000000000000114571465720375400222400ustar00rootroot00000000000000PNG  IHDR=2gAMA a cHRMz&u0`:pQ<bKGD pHYs00eʶtIME dIDATxklUʼnܜI%i)MzI v^CAݥ]JEa'vVvav0Ue ;@BJI\4v;7!Ms')>?O!4Ua_|a6P psf깂.>֨VY4>?Pm ,jxgg,(z#JM> ׊߁ B7[vnN~n $:W0"f!1\7t$kNozt>Yw^5a}^}U>DFײڨnu :k*).x hc;ekDB5·޸q#>,{Y{{{yꩧx-ń{[iru%&s;FB|ׯM;ycA߿Cp8R[oŁ[=Ukh v/ &nN:ERPA~x0ҟ);::عsu.H-V5ģq:əgG>wѝn~mW^y|D"na[Q=E>3̱194ڔ<ڈiN'픗k_ss3ΝĉJmTqI5ۀjs]šqjkk5gDT`dz;(v17A[V4@4eecn7^R MY+^>14{T%+JZ`QЂ6[͚J奣c~u%0ުaA aGU91*2!^?d숲{`4?fߚ /*ݎՕ֕*+WWRRSY4Q)hHV7 +5eovfs+F*qP9mveX ɝ,Xq&G@A X[M44ZJV3u#Ym{_<`A"bhG :ʲ~TDamY'uuI UvXV|;\ Jnj6&,|%>%V[yNx)A,&6tCή-[_KVu f[r)4-lT7go!e6D>ޭ jοscLMı9l֕R&7u[0\Z+ݬ}pm\ xd]G[Vܯۗ^=nƳգ|s6SX֡'$psn̹{t%h֕*{ nLs3JBz,vn8A;=NOꙊ1<Jǰ\,v6 NEs2:ө4O:VfjNz852v"Prr)_Yڄ 'h h%@wctx$zHUqx-0CM1M'}rKauVxVX^kȸn/kӭ d8AG VSAfi.w^Q:|Cǿv6ckiD"`z _ scVǦmB"R'gذ4AC z>>zܿ[jV,g6 ߻ȅ?\`LA1kS)8Pdx$NdYPe,1QH:? W/,tK " x)?@rjOH6ܟg8H+l^J9`nPBR\!DI.jF21J3vlHEV^< @mD2YE.gBe$w\~ N41n$sOL6Jߤ%tEXtdate:create2024-08-09T00:08:29+00:00@%tEXtdate:modify2024-08-09T00:08:29+00:001\OtEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/browserconfig.xml000066400000000000000000000003661465720375400217530ustar00rootroot00000000000000 #da532c elvish-0.21.0/website/favicons/favicon-16x16.png000066400000000000000000000013261465720375400212730ustar00rootroot00000000000000PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<PLTE___22n28{8&T&4^4;;UU%R%===VVVRRCC@@UU   QQ"L""J"TT==OOSS"FF)Z)>6v6>> ::  45u52m2CC   m|ZtRNSKJI("bKGD pHYsvvN{&tIME "fIDAT]0@t(6(F@F?fIvØEZmD_Ñ$ٜqC:l [N`*8 87nメnc!X+ qJ%tEXtdate:create2024-08-09T00:08:34+00:00%tEXtdate:modify2024-08-09T00:08:34+00:00=tEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/favicon-32x32.png000066400000000000000000000020501465720375400212620ustar00rootroot00000000000000PNG  IHDR DgAMA a cHRMz&u0`:pQ<MPLTE888  -A8  ___9JJUUTT:: JuJ8z8191*  6x6TT  +++ttt$N$II>>/g/SS 'V'AFF(PP3q33PPTTSS1m1 F ==KKBB??#N#%"4r4  UU+_+2n2*\*DDEE "L"+^+-d-> 6w6,'2o20::DD-c-"RR1l1     /  R tRNS*)('`bKGDi pHYsy(qtIME "f"IDAT8ˍWS1ZQDŊbG{/?]/ٽ9BM@g!0јXf`ɿAeU5bMm]^ YSs@1Νt@'c]=^} 0084,#Qc `L3z0/s_ЁU,irgVd5` 5|k[)w+c8I))/(S/e7']<|][^*|>˛?N  1pDŎ^N'J%tEXtdate:create2024-08-09T00:08:34+00:00%tEXtdate:modify2024-08-09T00:08:34+00:00=tEXtSoftwarewww.inkscape.org<WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/favicon.ico000066400000000000000000000353561465720375400205100ustar00rootroot0000000000000000 %6  % h6(0` $ompn *%%      );"K"E1@@UUUUUUUUUUUUUUAA4s4RRUUUUUUUUUUTTAA7MMUUUUUUUUUUUUUUGG  KKUUUUUUUUUUUUUUUUUUDDHHUUUUUUUUUUUUUUMM??UUUUUUUUUUUUUUUUUUUU8{8#N#UUUUUUUUUUUUSS$UUUUUUUUUUUUUUUU3o37"L"'DDUUUUUUUUUUUU ,b,UUUUUUUUUUUUUUMM!I!UUUUUUUUUUUU  FFUUUUUUUUUUUUUU4r4PPUUUUUUUUUU&  UUUUUUUUUUUUUUUU81m1UUUUUUUUUU;%Q%UUUUUUUUUUUUUURRUUUUUUUUUU&T&??UUUUUUUUUUUUUU::AAUUUUUUUU1l1 SSUUUUUUUUUUUUUU F CUUUUUUUU<<AUUUUUUUUUUUUUUTTOOUUUUUUGG7z7UUUUUUUUUUUUUU@@1m1UUUUUURRPPUUUUUUUUUUUUUU&T&%UUUUUUUUUUUUUUUUUUUUUUUU  EEUUUUUUUUUUUUUUUUUUUUGG$O$UUUUUUUUUUUUUUUUUUUU,b, RRUUUUUUUUUUUUUUUUUU(7z7UUUUUUUUUUUUUUUUMM1UUUUUUUUUUUUUUUU3o3  UUUUUUUUUUUUUUUU6#L#UUUUUUUUUUUUUUQQ< UUUUUUUUUU6w6,' PPUUUUUU"L"+^+UUUUUUUUUU  2n2UUUUUU*\*DDUUUUUUUUEE"UUUUUU4r4  UUUUUUUUUU+_+BBUUUU??#N#UUUUUUUUUU% F UUUUJJ==UUUUUUUUKKPPUUTTSSUUUUUUUU1m13q3UUUUUUUUUUUUUU3(UUUUUUUUUUUUPPFFUUUUUUUUUU8z8'V'UUUUUUUUUUA/g/UUUUUUUUSS IIUUUUUUUU>>+++tttUUUUUUUUUU$N$191*  6x6UUUUUUUUTT  JuJUUUUUUUUUUUUUU8z8___9JJUUUUUUUUTT:: 888  -A8  *(*)(  JI     4UUUU5u52m2TTUUCC6v6UU>> UUUU::  "UUFF)Z)UUUU>CCOOCCUUSS "J"UUTTUU== QQUUUU"L"@@UUUU  ===VVVRRUUCC4^4;;UUUU%R%___22n28{8&T&KJelvish-0.21.0/website/favicons/mstile-144x144.png000066400000000000000000000073241465720375400213130ustar00rootroot00000000000000PNG  IHDRFgAMA a cHRMz&u0`:pQ<bKGDtIME &ao8 uIDATx{l[?׎h$Mv )m@Pa J?`Lb/iE!4$4 cV6&k`TJ[(eJҍ4y6։ľ0)!=|O\;y(dm@h*,꒘G߀F4 4Yl]>7 c-stDs BhzxY|&,#LU'5g\(J aXEG<.=keF9'9&5$I:&5ι #I6`|:l6*V!:n5K H6m $lhw~X&.-XBjfQ" *$DR@]HIt!$хDR@]HIt!$хD{ Ur,v%45&HDfh B@---رN|>Da8HV:\WWS^Cժ*ɹs.B핵4lnnS55JhI'&zlyi_>?44wÇ}V\VӼmӿ|g~F2^B2E@ݻ⌾ رc7{o0O_~,p\^~__6l ="_D|6Ρbаva?xw 3TTRDOXg) TZZW[]_+KUỄBf"o[nEx$<i%q+T@7ni]SS"Ϳ, [hIEB> z9嘎]uPE"<5Idt:$P(&nYb3500 ĉ F28gG.EjkXw:| gmy:::]O2322T(+#<^@}b=k g4{_CK˝Pvzطo@zQC,*feJCO$>֡FT7n]e> \@ /bgؿ.],8”=^{7xGt?~lT>z|*[* m#0E@H[o{.!v"駟fΝEKj|"L(7fښh4?Lgg'/@8 g6nȣ>J"С!Xʛ]JJJX~=>MXW*w9[_gvrְ6&od[JU魈̖#>B_aҍE `67PT_ $rа9w;($@@(N4y|Yk ^@1򡜕Ζ5&ONF*su^!эIZMBӚu5֕RjU8w)PXuU5+h5^(ܥ_]ojA HT<A5fw=w1pWMi8_j˜V$Emk3mEHڪ^Sm vqc)}ˬt vb~' ~nHb%'&\qPA y+֬l3tQP 0JeQejHq*y!V F@.|2nS.l3`#yvr6QdjA-o/0\P`{őy lr՚Vg(֚wϘ]y5 P㵍5d7'9,w.vGUB._~QTRS.MF`~nY}իE?"Z@mڲN#WjaO ׊(zuAnbV@)ffW!!5Ք7T$P63Y(h`p[+jKWv7!scKު{7Agk7NJtԕzYɓ_[(rxuPz**t׳ 4 YiVvwFXV@tuM}$I=kx_onf-7|5VUN]\i#n{+so"{;VwMŘ=?dEWvܶ~7On|,#CD3X83t<9IM{t/ Ze\XZNgGFZXIqU6-#z1Ge~Od1ߜhj8袽dfQ尌IcϑIgQ\-]ؒaG{lQcO52YG@b, ~]!;Y F9sB۴&NNi'h>P|C8SR9o=C 2@cGDŽԄgqtSmڨTcG| $>|3Y.'jX%&H&P#*xx8N<'&QC_~>--II036cY@riK%i$ L<$dw!`DžEQiNLuF%ą$lE H ђD H ) .$B H `E;sf[!,`l+$e0 e9mIJWv[HIc-Xyߘmr<_>ZͶJb H>(l$~3YdYq`RRp .n]LnΘm$3O@ R[)UO;5nEBr@ИRbDZ؎z}t wf34b{f3x~3ߌ .IIWR ֙.f%K:/锤_J5] IKf*)%阤ݦ?42L L'K?+sVNI#206Ice393-+sm pnIK2agrvIkUN_xee- ) (#G% .iT¿.pƑ%cRBJ]teg @Q%drGn]. KRaCn'AIlR۽ @}tȭt(m (uGnQ6.qq47)I!E2#`F4UFp`6ml#8FԤ;T}}&''uY{JR Q}jmRmk*Te}**UyGRR+)%WM4?:K |Z㋦w J js+Zp8_|QO? KzהL&ojuPv.R@`Kq}kwƱ;'Ǟ={[o)A=EDwOwW JZ8b86`zp̙3jkk{SN:th׳T/|EY!Y0\<\x 6olknq:uJ1u[mjB꺿KqmE (eU1^ϫIɕ/Ԯgw:f=kQ2v!hծ5~Cp?p/?T?il00( * 6hܔxF56EpdYxAX^^WZtVZS˚Y}5zsfÛ\_(5lxxXkȈ/h.{YS4s ?۲E;lڶ7_]tJX/ %3Î?^ǎ$ gпifh[!I3f%ɩJ;ۋ`aox6|N>mkT曺~ /k:zhWZy7ÚtU:?XQGq-86DY+W… $Rseg^Gp9VM6UǗ۶MMզ7."8|,m5ф az"jD4Nr\KaUTdJ"8|.ӎ/7TR.ynrwų-/RЋP=,F-[E5mjRs_k˧MϹPÛ 꺿u4v5acMkxd?g^Cp[EoFs{YW'ioUͺӛ >}[5O N#w>@Bwyxd_D ޼އ{Zwp#MMZuFk7>He [VV2xz5t2>NUW Epn,\^p4W jo5آ~|UɕlkQM zm{}qlQxJg''hn0[Pァv۞o$^L[#8<.zoS;WiߟVLVC{fbJ26squUTW(|7auԾk.+aؿ(*I˓7[)S,ãzic~x^cXXm z+GGEDl*\ᑂ\MIrڝ^\(Ouԩ}߸UpL}:Uk(AmR 7_㐤ćYG!8<& j0b+a)~=~uwт̮H(:ᄬx-wX 8 MrVfiiTv{o2_"Dpxʖn)xx?~ۦL!»^T?Svrq,O.k+긛S #zqdzkߥ+R.›)R^/jrep.\Ȳr5U$i˙[g)TEu?=04p5̵lwc˫[yZ]rwOu:ҕ%+iQcw"#=G싨yKzZ[Y]o<ŷ/j돷Եv:tcϭ/6v6A R '$%yĒF3ZR@pPMH6>X/8$klSڶ #8ʌVVfV~JloQf)Ă#>+>p y#8L|>U^3]x> Մ\(&8euiOŐN9%W7?6ә>K?nǖϦTw|QzOVC=㭎uJKӚ|ZN\Jsƅq[i>حUJ+Pj%%+aiuqUVRr%d,)+aݲ%en:ҵ6ԺU{"ۡ ;M[i--xRcie&wVqldɭ#PҲuĴ`(j6R.*Tr9صGZM.c# vNnstIYpqml#8Fpf yGn @LlRGpPtMPN.EJjsJ$B/,I= S D%tEXtdate:create2024-08-09T00:08:39+00:00'm%tEXtdate:modify2024-08-09T00:08:39+00:00z\WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/mstile-310x150.png000066400000000000000000000105631465720375400213020ustar00rootroot00000000000000PNG  IHDR.tgAMA a cHRMz&u0`:pQ<bKGDtIME )%`IDATx[}]zk ӀMmJB"EyFUC^(#xh/$ڪTT % i$8@S|+{s`mwϜH#Ğ33O]$$KrGMIV&Z܉$I^M3ɿ$_vQP;H2f$O$_hL_eK5gU fM_-l- $)KRe0&sK3)Re(ɭerAT4Fo}dѡ ٮEVҖw.f3T(e@}`6\X.`6\X}e@]]v095%$eŪ]pm*4 h 4i.@\!MCpaa]wݕk69rH^|G?Ĕ1#]kxt΂ 299'sɜ+dbl"ѡ*αWeS"b5l<ù2w~С<#yG3>>~랗g٦eYػ0/J{=CGu: b5$lݺ5{ر#_җrwVilwCTiWgǞ7fp#NK\[ >\Pƒ /dٲeΝ;O~2YuE6ms:{;rBFNg?ߐĵ ^v0=\+Jvؑ7Ni5kddd$;w̖?ْ+a'c~GV߹:#o oil"hb_e˖iC;Z6msVΪR 4/ؗiՕ{M*J<% ZP$@۾}ֿsjߩXБ4T[[[zzzf+W讣:/.y.ФjFFFfxD!o>f:{2Y]rݒG7eٍZ]Ug1`VM'OS" 4{wZ-!OW|[ߚ>gHwp1 ###m۶t2 cAGV}lU)t.t\Qʾ{fEhu p߽}[>\:::hI .Es?t/OAk\Zwn9|`!m]Lp.hѪEYiٔ80/xYrÒ_bEhU pAYLqȑԽJDpΫ-kZ3Ny:IԝW}=oj+Ւ$IsjugsO:3/P.8L}ܡCeH>==1""8+fM˧_wPO pN?3rdsmtZ|@[G[޵vZ=;s*^ܮYzI.;R}gEuf0:h5 ӹ)*J3\.YXLw+ь{ZKpβuTqIZC?=THZ].:ڲSPcՌ=r׊z}.Z]}why=CW^kd:E@]?qF돜9kcc9hĤB Iҵ+n\6m FfJp$|oc_qyfgsO:0"Ȝsg 7Nֽ9mYE2"Y? t.V @kh5_y7Qr񮢤Qt>ڗm!@[Yس.ۺXWQsg4Ss}7^=fn6hao^m%{vw}ݏgýga??gIۜ_:?]kһ7s\ 4-w[o_8WUYysGSo亯\7gY7vʢUrzh ]Eb<%~SCK ]:?L 4/ZH[<ג3\dףzYv%@ )rԪ>/2 ݅tj\&K czy^n9( hZȩBfhNM^=Z~_ޫc~G!7!@+% K{\v<39sL?tl(/ ?ԍǡ}h::K}|z,OSY+mPZc˾eIu'1B.?ܵ&>tkjZ21ѷ\[ >\P.T+I-Szlmm鼺3e9XgF3>8 5S{Oebt][ >\PIԓk+͹@\!MCp4 Us4s((tl&@] fl&@^)ղ Lpb,ٲ ̰P$⏄VQM6 u ] T((MI^?fj B ە.= ce$B(L.@cL$eB %bfe2,b9@ I/]̖73=ފL9-S[&mnIDfX&{&[̀x.2yǒܐwg,$'39૙_cEs?T:S %tEXtdate:create2024-08-09T00:08:41+00:00 %tEXtdate:modify2024-08-09T00:08:41+00:00PWzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/mstile-310x310.png000066400000000000000000000207611465720375400213010ustar00rootroot00000000000000PNG  IHDR..JgAMA a cHRMz&u0`:pQ<bKGDtIME ( IDATxyeWatO/ӳOlE dbl !R,)KTIA*p ;!W`ǒѾKhf^G#YBuOw}*Mkw^=瞓V:$W'yUI.L2dA?s8ɮ$?H=I6abS_Nr}mI:KKrOo&RK&$Ore0nOGIdt8ŅfԗI~+ɚa('|!P0&L @{2ɇy b bEgI~-ɞA Q\hoU''StS:m$#S2i^_Jr,J%$6Atdj9%2Y:IqZAi<$_+P§!4$(h7%7CLm` en$ߎ b"S^tڇB,O`jwWgjGh_hEZ:3&к~6K).[o%Y_:PWO&(p 6CEۯEiv1KB=u%yҒ$'?džJXuŪ\h,ɢouo^?8P:P'c$پ} },t ;܉{d_>7n_Y-XxlG[Tp뚟Z?`&'Zf[,,).pw_I{%5&l맳}cssX:ì;4pկ|e%ɬ$yOchl:V_eڐ}?7X(zշ/6,*h0N[nmVַr]w5x';w(r~8|#o1r76\@Q\4|ӟn1?塇j{g冋_kӣ|-Ґc}>"9zl4ȱg]h' hnRiUutFq2u ׶׊݋KJIqKG(bQ\uweޮ U@[m7DU@k%EP- %.ɢJ(<XN}%\@P\Muvf5kK(V"TmkKg_gM<ԆJQ hC 7.̒-V}ޜ9YZ.B(.І6I?pTmfNv]]+}Ӷ6] P\ͬzMzGd3Yz13P\4b=ɡ*}Ӷ*AS\_;?.^Vc ePvܾNy.h#޶!4M&w,}ֿ? -(8 DGgG3G$ޡҧ=m@sS\MjUu8L&;]\).&g;<[tm`,e3v|OKV˪+ AR\ lxjY\텡$ҧ?m 4/Z\mN-߲!:܉yvQYalhR LކDqǎLNTkݎ vt $hqty#GKcМhaָ;'$;c+/_.BQ\m|Ɔx|;.I5̝?7.).Тjc'nL33\Gq5m0}} =Nxe FqII2~b<#GNX\歘-gIO/k?}9܉ߖ1\Eqm{O_\&'&]%-3@sQ\Ԓׯoat%pђK;И3S\ v0~ܡCg7ٕߖ%.w-f79dlh,{[$߹0\Cqҳg3T$;pW/OW_W@h)߲rwI\LGWGVnE@hd[ %о3qy|`3*A3P\E,n$92;榋fO!Fbg31ŪXtWW]lE(Mqн;__n(ch =]v<Zק܏N}d juR+ڛUWZ⿤Eˢ Jǀ@-dy毝_4tdσ{2v'6 EPw /.uMQ\O8\x[1t h[ TwlLmN٢Çgy;ܙ-5]2Ύrdrb2#Gf#GF{J´.r5?&={JLNId ]6] ~dD{gK´utvdp`ЖEeEKKH2;hHӘWtJP\$q{.lЦP:}1^pйqIXt׼ \2P:*f[6tPQ>xwnEx TImjf2CEuOVѵ"42_Sv_4CEI . mEq 9/efowGKδ~Ml %XΞt/N`_毙Xk/D=yi@SS\:jd ߵ9+h^lYCyά92Igog^vOjl[r)@SS\%.%]қdř;nXg%Ic-.bWsV'w\P+Jpz TK毮pq9˧gfbC{+Z#@SS\ޮtUwĶ^CE{??ѳ'sTc@AU.-I}莣y>͆[nAд(ťCE{k<[Tjy S:9t/3ja}#@R\#FqI|GKrCTNԓU}z-@w2C{77mQ^85 >01fdxG{hhP-}u).Pd>^:ƌ4j'xDn[ony' Fq?}tiSȽwon3|pb ݿ{w}@r? ;聬z 6]go{6{ٛ~l|ttVw##yglo+43 ;#H\`G@_~E־qme ̱מDƎW{64LMTbz/?]G9?qwGr]uo^ށұ^fH<҉ZjВO5f7yS?F?416=Ɔe}:O8fk uPـ@[S\P\P\P\P\P\P\P\P\P\P\P\P\⎗@kR\)֤PްԅB=,(uPK{tZB=xP p@q@k@KdA"F,IrtZ;.Ñ$sWDq^Y:P_@2TD\ƠLfAhMP/'tnB).J=u6>ԕ$J$2Tԅ;.h+h:sDžzM$K$'9Q:m(oG IZ:P7_IK=*Q^:Gqo'YAL=E Pג$yC ~JvI25wPǡi$44gb-K3%S]Z:2_:-J MAqYI$oNa80  WWW𫫫777.,`,<>/h/3!<<+_+|||@@@GGGG$O$  CCC  &T&666ѲEEAA<< 枉hLn.Ϝ%FReOGG̩?B}rrσOH^U0J{@'~~BTBno"_Tsj?!qaULժרiUNQPzEkB4 @52XҸihch3d@s--HV2۠JvEZ:0CTU֝{ zt*oČz}"lf ? mC`(`p}t$<dkqpA -&TD6}Yh"&f HCccx L1,k- b,g hx&` |+e XA X^! f-`\)Ҳ   Yg +T/di+L{l}{>&R_8pVx^# y?JAq~R BN)|vI@-T:8e/ә_ @}lyY|.\D,]2_ pj*\ܐUͳADY-mx `~k{)cC " _}OAS<#) % C3 ) /WW։((Z*ƀXEtF)⥹u1Xc&#lO)4+Gߌ>}&Z$}=_C8+8L7.> u@MbҚdH3>"wT@Qb RYPJEP ࡪ_r#U/#{W/WHG9^}Z}}+xxxxxxxxE:p&ȫ%tEXtdate:create2024-08-09T00:08:37+00:000%tEXtdate:modify2024-08-09T00:08:37+00:00E'WzTXtRaw profile type iptcx qV((OIR# .c #K D4d#T ˀHJ.tB5IENDB`elvish-0.21.0/website/favicons/site.webmanifest000066400000000000000000000006521465720375400215500ustar00rootroot00000000000000{ "name": "", "short_name": "", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } elvish-0.21.0/website/fonts/000077500000000000000000000000001465720375400156745ustar00rootroot00000000000000elvish-0.21.0/website/fonts/FiraMono-Bold.woff2000066400000000000000000000165441465720375400212430ustar00rootroot00000000000000wOF2OTTOd $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.21.0/website/fonts/SourceSerif4-Semibold.woff2000066400000000000000000000333201465720375400227130ustar00rootroot00000000000000wOF2OTTO6 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.21.0/website/fonts/SourceSerif4-SemiboldIt.woff2000066400000000000000000000351401465720375400232120ustar00rootroot00000000000000wOF2OTTO:` \: 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.21.0/website/gen-fonts.elv000066400000000000000000000021351465720375400171540ustar00rootroot00000000000000# 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.21.0/website/get/000077500000000000000000000000001465720375400153225ustar00rootroot00000000000000elvish-0.21.0/website/get/all-binaries.md000066400000000000000000000346431465720375400202200ustar00rootroot00000000000000This page collects links to all prebuilt Elvish binaries. The URLs follow the same pattern of `https://dl.elv.sh/{os}-{arch}/elvish-{version}.{ext}`, where `{ext}` is `.zip` for Windows and `.tar.gz` for all other OSes. These binaries are also available on the TUNA mirror site. # Current versions
    Version amd64 386 arm64
    HEAD (draft release notes) @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.20.1 (release notes) @dl Linux linux-amd64/elvish-v0.20.1.tar.gz @dl macOS darwin-amd64/elvish-v0.20.1.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.20.1.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.20.1.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.20.1.tar.gz @dl Windows windows-amd64/elvish-v0.20.1.zip @dl Linux linux-386/elvish-v0.20.1.tar.gz @dl Windows windows-386/elvish-v0.20.1.zip @dl Linux linux-arm64/elvish-v0.20.1.tar.gz @dl macOS darwin-arm64/elvish-v0.20.1.tar.gz
    # Old versions The following old versions are no longer supported. They are only listed here for historical interest.
    Version amd64 386 arm64
    0.20.0 (release notes) @dl Linux linux-amd64/elvish-v0.20.0.tar.gz @dl macOS darwin-amd64/elvish-v0.20.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.20.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.20.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.20.0.tar.gz @dl Windows windows-amd64/elvish-v0.20.0.zip @dl Linux linux-386/elvish-v0.20.0.tar.gz @dl Windows windows-386/elvish-v0.20.0.zip @dl Linux linux-arm64/elvish-v0.20.0.tar.gz @dl macOS darwin-arm64/elvish-v0.20.0.tar.gz
    0.19.2 (release notes) @dl Linux linux-amd64/elvish-v0.19.2.tar.gz @dl macOS darwin-amd64/elvish-v0.19.2.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.19.2.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.19.2.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.19.2.tar.gz @dl Windows windows-amd64/elvish-v0.19.2.zip @dl Linux linux-386/elvish-v0.19.2.tar.gz @dl Windows windows-386/elvish-v0.19.2.zip @dl Linux linux-arm64/elvish-v0.19.2.tar.gz @dl macOS darwin-arm64/elvish-v0.19.2.tar.gz
    0.19.1 (release notes) @dl Linux linux-amd64/elvish-v0.19.1.tar.gz @dl macOS darwin-amd64/elvish-v0.19.1.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.19.1.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.19.1.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.19.1.tar.gz @dl Windows windows-amd64/elvish-v0.19.1.zip @dl Linux linux-386/elvish-v0.19.1.tar.gz @dl Windows windows-386/elvish-v0.19.1.zip @dl Linux linux-arm64/elvish-v0.19.1.tar.gz @dl macOS darwin-arm64/elvish-v0.19.1.tar.gz
    0.18.0 (release notes) @dl Linux linux-amd64/elvish-v0.18.0.tar.gz @dl macOS darwin-amd64/elvish-v0.18.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.18.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.18.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.18.0.tar.gz @dl Windows windows-amd64/elvish-v0.18.0.zip @dl Linux linux-386/elvish-v0.18.0.tar.gz @dl Windows windows-386/elvish-v0.18.0.zip @dl Linux linux-arm64/elvish-v0.18.0.tar.gz @dl macOS darwin-arm64/elvish-v0.18.0.tar.gz
    0.17.0 (release notes) @dl Linux linux-amd64/elvish-v0.17.0.tar.gz @dl macOS darwin-amd64/elvish-v0.17.0.tar.gz @dl FreeBSD freebsd-amd64/elvish-v0.17.0.tar.gz @dl NetBSD netbsd-amd64/elvish-v0.17.0.tar.gz @dl OpenBSD openbsd-amd64/elvish-v0.17.0.tar.gz @dl Windows windows-amd64/elvish-v0.17.0.zip @dl Linux linux-386/elvish-v0.17.0.tar.gz @dl Windows windows-386/elvish-v0.17.0.zip @dl Linux linux-arm64/elvish-v0.17.0.tar.gz @dl macOS darwin-arm64/elvish-v0.17.0.tar.gz
    0.16.3 (release notes) @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
    0.16.2 (release notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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 notes) @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.21.0/website/get/default-shell.md000066400000000000000000000130051465720375400203740ustar00rootroot00000000000000 # Configuring the terminal to run Elvish This is the recommended way to use Elvish as your default shell. ## macOS terminals
    Terminal Instructions
    Terminal.app
    1. Open Terminal > Preferences.
    2. Ensure you are on the Profiles tab (which should be the default tab).
    3. In the right-hand panel, select the Shell tab.
    4. Tick Run command, put the path to Elvish in the textbox next to it, and untick Run inside shell.
    iTerm2
    1. Open iTerm > Preferences.
    2. Select the Profiles tab.
    3. In the right-hand panel, change the dropdown under Command from Login Shell to either Custom Shell or Command, and put the path to Elvish in the textbox next to it.
    ## Linux and BSD terminals
    Terminal Instructions
    GNOME Terminal
    1. Open Edit > Preferences.
    2. In the right-hand panel, select the Command tab.
    3. Tick Run a custom command instead of my shell, and set Custom command to the path to Elvish.
    Konsole
    1. Open Settings > Edit Current Profile.
    2. Set Command to the path to Elvish.
    XFCE Terminal
    1. Open Edit > Preferences.
    2. Tick 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 to change the shell. Depending on your DE, you can either create a wrapper script or [modify the desktop file](https://wiki.archlinux.org/title/desktop_entries#Modify_desktop_files):
    Terminal Instructions
    LXTerminal Pass --command $path_to_elvish.
    rxvt Pass -e $path_to_elvish.
    xterm Pass -e $path_to_elvish.
    ## tmux Add the following to `~/.tmux.conf`: ```tmux if-shell 'which elvish' 'set -g default-command elvish' ``` This only launches Elvish if it's available, so it's safe to have in a `.tmux.conf` that you sync with machines where you haven't installed Elvish yet. ## Windows terminals
    Terminal Instructions
    Windows Terminal
    1. Press Ctrl+, to open Settings.
    2. Select Add a new profile from the left sidebar, and click New empty profile.
    3. Set Name to “Elvish” and Command line to the path to Elvish.
    4. Select Startup from the left sidebar, and set Default profile to Elvish.
    5. Hit Save.
    ConEmu
    1. Press Win+Alt+T to open Startup Tasks.
    2. Click ± below the list of existing tasks.
    3. Set the name to “Elvish”, enter the path to Elvish in the textbox below Commands, and tick Default task for new console.
    4. Click Save settings.
    ## VS Code Open the command palette and run "Open User Settings (JSON)". Add the following: ```json "terminal.integrated.defaultProfile.linux": "elvish", "terminal.integrated.profiles.linux": { "elvish": { "path": "elvish" }, } ``` Change `linux` to `osx` or `windows` depending on your operating system. See [VS Code's documentation](https://code.visualstudio.com/docs/terminal/profiles) for more details. # Changing your login shell On Unix systems, you can also use Elvish as your login shell. Run the following Elvish snippet: ```elvish use runtime if (not (has-value [(cat /etc/shells)] $runtime:elvish-path)) { echo $runtime:elvish-path | sudo tee -a /etc/shells } chsh -s $runtime:elvish-path ``` You can change your login shell back to the system default with `chsh -s ''`. ## Dealing with incompatible programs Some programs invoke the user's login shell assuming that it is a traditional POSIX-like shell, so they may not work correctly if your login shell is Elvish. The following programs have been reported to have issues: - GDB (see [#1795](https://b.elv.sh/1795)) - The vscode-neovim extension (see [#1804](https://b.elv.sh/1804)) Such programs usually rely on the `$SHELL` environment variable to query the login shell. For CLI applications, you can create an alias in your `rc.elv` that forces it to a POSIX shell, like the following: ```elvish fn gdb {|@a| env SHELL=/bin/sh gdb $@a } ``` There is no universal way to override environment variables for GUI applications; it depends on the GUI environment and possibly the application itself. It may be easier to switch the login shell back to the system default and configure your terminal to launch Elvish. elvish-0.21.0/website/get/index.toml000066400000000000000000000005271465720375400173320ustar00rootroot00000000000000prelude = "prelude" extraCSS = ["prelude.css"] extraJS = ["prelude.js"] autoIndex = true [[articles]] name = "default-shell" title = "Using Elvish as your default shell" [[articles]] name = "package-manager" title = "Installing Elvish with a package manager" [[articles]] name = "all-binaries" title = "All binaries available for download" elvish-0.21.0/website/get/package-manager.md000066400000000000000000000074641465720375400206620ustar00rootroot00000000000000 Installing Elvish with a package manager allows you to upgrade Elvish alongside the rest of your system, but may not give you the latest version. If the package manager you use doesn't have the latest version, you are strongly recommended to the [official binary](./) instead. For a comprehensive list of packages and their freshness, see [this Repology page](https://repology.org/project/elvish/versions). # Arch Linux ![Arch package](https://repology.org/badge/version-for-repo/arch/elvish.svg) To 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 ``` # Debian / Ubuntu ![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/elvish.svg) ![Debian Unstable package](https://repology.org/badge/version-for-repo/debian_unstable/elvish.svg) ![Ubuntu 23.10 package](https://repology.org/badge/version-for-repo/ubuntu_23_10/elvish.svg) ![Ubuntu 24.04 package](https://repology.org/badge/version-for-repo/ubuntu_24_04/elvish.svg) 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 ``` # Fedora ![Fedora 40 package](https://repology.org/badge/version-for-repo/fedora_40/elvish.svg) ![Fedora Rawhide package](https://repology.org/badge/version-for-repo/fedora_rawhide/elvish.svg) Elvish is packaged for [Fedora](https://packages.fedoraproject.org/pkgs/elvish). To install it with `dnf`: ```elvish dnf install elvish ``` # macOS Elvish is packaged by both [Homebrew](https://brew.sh) and [MacPorts](https://www.macports.org). ![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/elvish.svg) To install from Homebrew: ```elvish # Install latest packaged release brew install elvish # Or install HEAD: brew install --HEAD elvish ``` ![MacPorts package](https://repology.org/badge/version-for-repo/macports/elvish.svg) To install from MacPorts: ```elvish sudo port selfupdate sudo port install elvish ``` # Windows ![Scoop package](https://repology.org/badge/version-for-repo/scoop/elvish.svg) 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 ![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/elvish.svg) 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 ``` # NetBSD / pkgsrc ![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/elvish.svg) Elvish is [available in pkgsrc](https://pkgsrc.se/shells/elvish). To install from a binary package, run the following command: ```elvish pkgin install elvish ``` To build the elvish package from source instead: ```elvish cd /usr/pkgsrc/shells/elvish make package-install ``` # OpenBSD ![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/elvish.svg) Elvish is available in the official OpenBSD package repository. This will install the latest packaged release: ```elvish doas pkg_add elvish ``` # NixOS (nix) ![nixpkgs stable 23.11 package](https://repology.org/badge/version-for-repo/nix_stable_23_11/elvish.svg) ![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/elvish.svg) 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 ``` elvish-0.21.0/website/get/prelude.css000066400000000000000000000043331465720375400174770ustar00rootroot00000000000000form { margin: -32px 0 16px; padding: 20px 16px 16px; background-color: #f0f0f0; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; } .dark form { background-color: #222; } input[type="radio"] { position: fixed; opacity: 0; pointer-events: none; } .control { display: flex; margin-bottom: 8px; } .control > header { flex: 0 0 88px; display: flex; align-items: center; justify-content: center; background-color: black; color: white; border: 1px solid black; border-right: none; border-top-left-radius: 10px; border-bottom-left-radius: 10px; } .widgets { display: flex; /* Constrain the background of children to within the rounded corner. The content should never actually overflow. */ overflow: hidden; flex-wrap: wrap; border: 1px solid black; border-left: none; border-top-right-radius: 10px; border-bottom-right-radius: 10px; } .option { padding: 6px 8px; color: black; background-color: white; cursor: pointer; /* When the options need multiple rows, grow to fill each row and center the text. */ flex: 1 0 auto; text-align: center; } .option:has(> input:disabled) { cursor: not-allowed; background-image: repeating-linear-gradient( -45deg, white, white 8px, #d0d0d0 8px, #d0d0d0 16px ); } .option:hover { background: #f0f0f0; } .option:has(> input:checked) { background: var(--blue-800); color: white; } details { padding: 4px 6px; margin: 0 -6px; background: #dadada; border-radius: 6px; } .dark details { background: #333; } details > summary { cursor: pointer; } .advanced { padding-top: 8px; } .advanced .control input[type="text"] { font-size: 0.8em; font-family: var(--code-font); padding: 2px 4px; width: 228px; margin: 6px; } /* Needed for the around "sudo" and "doas" options in dark mode. */ .advanced code { color: unset; background-color: unset; } .small-print { font-size: 0.85em; padding-left: 12px; /* The standard margin-bottom of block elements, needed due to the rule on .small-print p below. */ margin-bottom: 12px; } .small-print p { /* If the small-print text has multiple paragraphs, make them vertically more compact. */ margin-bottom: 6px; } elvish-0.21.0/website/get/prelude.js000066400000000000000000000175601465720375400173310ustar00rootroot00000000000000document.addEventListener('DOMContentLoaded', main); const binaryAvailable = new Set([ 'linux-amd64', 'linux-386', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'freebsd-amd64', 'netbsd-amd64', 'openbsd-amd64', 'windows-amd64', 'windows-386', ]); function main() { // Set up change detection. for (const e of document.querySelectorAll('input')) { e.addEventListener('input', (event) => { const el = event.target; onChange(el.name, el.value); }); } // Populate os and arch, either from localStorage or by auto-detection. const os = tryGetLocalStorage('os'); const arch = tryGetLocalStorage('arch'); if (os && arch && binaryAvailable.has(os + '-' + arch)) { select('os', os); select('arch', arch); } else { autoDetectOsAndArch(); } // Populate version and dir from localStorage. const version = tryGetLocalStorage('version'); if (version) { select('version', version); } // Populate dir, sudo and mirror from localStorage, and open the
    if // they have non-default values. var openDetails = false; const dir = tryGetLocalStorage('dir'); if (dir) { document.querySelector('input[name="dir"]').value = dir; onChange('dir', dir); openDetails = true; } const sudo = tryGetLocalStorage('sudo'); if (sudo && sudo !== 'sudo') { select('sudo', sudo); openDetails = true; } const mirror = tryGetLocalStorage('mirror'); if (mirror && mirror !== 'official') { select('mirror', mirror); openDetails = true; } if (openDetails) { document.querySelector('details').open = true; } } function onChange(name, value) { trySetLocalStorage(name, value); // Update input controls. if (name === 'os') { const os = value; // Disable unsupported architectures. for (const element of document.querySelectorAll('input[name="arch"]')) { element.disabled = !binaryAvailable.has(os + '-' + element.value); if (element.disabled && element.checked) { const fallbackArch = fallbackArchForOs(os); select('arch', fallbackArch, {suppressOnChange: true}); // Because we suppressed the recursive call to onChange, we have to // store the new arch manually. trySetLocalStorage('arch', fallbackArch); } } // Update directory placeholder and the installation instruction. const $dir = document.querySelector('input[name="dir"]'); const $where = document.querySelector('#where'); if (os === 'windows') { $dir.placeholder = '$Env:USERPROFILE\\Utilities'; $where.innerText = 'PowerShell'; } else { $dir.placeholder = '/usr/local/bin'; $where.innerText = 'a terminal'; } } // Update outputs. const $form = document.querySelector('form'); const f = new FormData($form); const $script = document.querySelector('#script'); $script.innerHTML = genScriptHTML( f.get('os'), f.get('arch'), f.get('version'), f.get('dir') || document.querySelector('input[name="dir"]').placeholder, f.get('sudo'), f.get('mirror')); } function genScriptHTML(os, arch, version, dir, sudo, mirror) { const host = mirror === 'tuna' ? 'mirrors.tuna.tsinghua.edu.cn/elvish' : 'dl.elv.sh'; const urlBase = `https://${host}/${os}-${arch}/elvish-${version}`; if (os === 'windows') { const url = link(urlBase + '.zip'); const renameCmd = version === 'HEAD' ? '' : `Move-Item -Force elvish-${version}.exe elvish.exe\n`; return `& { md "${dir}" -force > $null $UserPath = [Environment]::GetEnvironmentVariable("PATH", "User") if (!(($UserPath -split ';') -contains "${dir}")) { [Environment]::SetEnvironmentVariable("PATH", $UserPath + ";${dir}", "User") $Env:PATH += ";${dir}" } cd "${dir}" Invoke-RestMethod -Uri '${url}' -OutFile elvish.zip Expand-Archive -Force elvish.zip -DestinationPath . ${renameCmd}rm elvish.zip }`; } else { const url = link(urlBase + '.tar.gz'); const sudoPrefix = sudo == 'dont' ? '' : sudo + ' '; if (version === 'HEAD') { // The filename inside HEAD archives are just called "elvish", so we can // just let curl write to stdout and extract it. return highlightSh(`curl -so - ${url} | ${sudoPrefix}tar -xzvC ${dir}`); } else { return highlightSh(`{ curl -o elvish-${version}.tar.gz ${url} tar -xzvf elvish-${version}.tar.gz ${sudoPrefix}cp elvish-${version} ${dir}/elvish rm elvish-${version}.tar.gz elvish-${version} }`); } } } function link(s) { return `${s}`; } function highlightSh(s) { // Use a simplistic algorithm to color command names and pipes green. return s.replace(/(^|\|\s*)[\w_-]+/mg, '$&') } function autoDetectOsAndArch() { const os = detectOs(navigator.platform); if (os) { select('os', os); // Select fallback and trigger change detection first in case the promise // errors or never resolves. select('arch', fallbackArchForOs(os)); let dataPromise = Promise.resolve(); if (navigator.userAgentData && navigator.userAgentData.getHighEntropyValues) { dataPromise = navigator .userAgentData.getHighEntropyValues(['architecture', 'bitness']); } dataPromise.then((data) => { const arch = detectArch(navigator.platform, data) || fallbackArchForOs(os); if (binaryAvailable[os+'-'+arch]) { select('arch', arch); } }).catch(() => { // Do nothing }); } else { // Use fallback and trigger change detection. select('os', 'linux'); select('arch', fallbackArchForOs('linux')); } } // Detects GOOS from navigator.platform. Partially based on // https://stackoverflow.com/a/19883965/566659. // // There is a better defined value in navigator.userAgentData.platform, but only // Chrome supports it. function detectOs(p) { if (p.match(/^linux/i)) { return 'linux'; } else if (p.match(/^mac/i)) { return 'darwin'; } else if (p.match(/^freebsd/i)) { return 'freebsd'; } else if (p.match(/^netbsd/i)) { // Not in the StackOverflow answer, but a reasonable guess return 'netbsd'; } else if (p.match(/^openbsd/i)) { return 'openbsd'; } else if (p.match(/^win/i)) { return 'windows'; } } // Detects GOARCH from navigator.platform and the high entropy data (if // available). function detectArch(p, data) { if (data) { const arch = { arm_64: 'arm64', x86_64: 'amd64', x86_32: '386' }[data.architecture + '_' + data.bitness]; if (arch) { return arch; } } if (p.match(/aarch64/i)) { return 'arm64'; } else if (p.match(/x86_64|amd64/i)) { return 'amd64'; } else if (p.match(/i[3456]86/i)) { return '386'; } } function fallbackArchForOs(os) { if (os === 'darwin') { return 'arm64'; } else { return 'amd64'; } } function select(name, value, opts) { const element = document.querySelector( `input[name="${name}"][value="${value}"]`) if (element) { element.checked = true; if (opts && opts.suppressOnChange) { // Don't call onChange } else { onChange(name, value); } } } // localStorage may not be supported by the browser, or its access may be denied // due to security settings. Wrap it to swallow exceptions. function trySetLocalStorage(key, value) { try { localStorage.setItem(key, value); } catch (e) { } } function tryGetLocalStorage(key) { try { return localStorage.getItem(key) } catch (e) { } } function copyScript(event) { event.preventDefault(); // Based on https://stackoverflow.com/a/48020189/566659 window.getSelection().removeAllRanges(); // clear current selection const range = document.createRange(); range.selectNode(document.getElementById("script")); window.getSelection().addRange(range); // select text document.execCommand("copy"); window.getSelection().removeAllRanges();// clear selection again const oldText = event.target.innerText; event.target.innerText = 'copied!'; setTimeout(() => { event.target.innerText = oldText; }, 1500); } elvish-0.21.0/website/get/prelude.md000066400000000000000000000105471465720375400173130ustar00rootroot00000000000000
    OS
    CPU
    If your OS/CPU combination is missing or grayed out, you may still be able to build Elvish from source.
    Version
    0.20.1 is the [latest release](../blog/0.20.1-release-notes.html). Suitable if you prefer to only update occasionally or need a stable scripting environment. HEAD is the latest development build. It has the freshest features and is stable enough for interactive use.
    Customize the script
    Install to
    Sudo
    Choose “don’t use” if you are running as root or installing to a directory you can write to. No effect on Windows.
    Mirror
    The TUNA mirror site is hosted in Tsinghua University, Beijing, China.
    Run the following in a terminal to install Elvish (copy to clipboard):
    
    
    Alternative, click the link above to download the archive and unpack it in directory in `PATH`. More topics about installing Elvish:
    Enable JavaScript to generate an installation script for your platform. Alternatively, find your binary for your platform in the [all binaries](all-binaries.html) page and unpack it manually.
    elvish-0.21.0/website/go.mod000066400000000000000000000004521465720375400156520ustar00rootroot00000000000000module src.elv.sh/website go 1.21 require ( github.com/BurntSushi/toml v1.3.2 github.com/creack/pty v1.1.21 src.elv.sh v0.19.2 ) require ( github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect ) replace src.elv.sh => ../ elvish-0.21.0/website/go.sum000066400000000000000000000020431465720375400156750ustar00rootroot00000000000000github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= elvish-0.21.0/website/home.md000066400000000000000000000106211465720375400160150ustar00rootroot00000000000000
    **Elvish** (*noun*): 1. A powerful scripting language. 2. A shell with useful interactive features built-in. 3. A statically linked binary for Linux, BSDs, macOS or Windows.
    Powerful modern shell scripting
    Write readable and maintainable scripts - no cryptic operators, no double-quoting every variable. ```elvish jpg-to-png.elv [(explainer)](learn/scripting-case-studies.html#jpg-to-png.elv) for x [*.jpg] { gm convert $x (str:trim-suffix $x .jpg).png } ``` Power up your workflows with data structures and functional programming. ```elvish update-servers-in-parallel.elv [(explainer)](learn/scripting-case-studies.html#update-servers-in-parallel.elv) var hosts = [[&name=a &cmd='apt update'] [&name=b &cmd='pacman -Syu']] # peach = "parallel each" peach {|h| ssh root@$h[name] $h[cmd] } $hosts ``` Catch errors before code executes. ```elvish-transcript Terminal: elvish [(explainer)](learn/scripting-case-studies.html#catching-errors-early) ~> var project = ~/project ~> rm -rf $projetc/bin compilation error: variable $projetc not found ``` Command failures abort execution by default. No more silent failures, no more `&&` everywhere. ```elvish-transcript Terminal: elvish [(explainer)](learn/scripting-case-studies.html#command-failures) ~> gm convert a.jpg a.png; rm a.jpg gm convert: Failed to convert a.jpg Exception: gm exited with 1 [tty 1]:1:1-22: gm convert a.jpg a.png; rm a.jpg # "rm a.jpg" is NOT executed ```
    Run it anywhere
    Elvish comes in a single statically linked binary for your laptop, your server, your PC, or your Raspberry Pi. ```elvish-transcript Terminal: Raspberry Pi ~> wget dl.elv.sh/linux-arm64/elvish-HEAD.tar.gz ~> tar -C /usr/local/bin -xvf elvish-HEAD.tar.gz elvish ~> elvish ``` Use Elvish in your CI/CD pipelines. Convenient shell syntax and modern programming language - why not both? ```yaml github-actions.yaml steps: - uses: elves/setup-elvish@v1 with: elvish-version: HEAD - name: Run something with Elvish shell: elvish {0} run: | echo Running Elvish $version ```
    Interactive shell with batteries included
    Press Ctrl-L for directory history, and let Elvish find `java/com/acme/project` for you. ```ttyshot Terminal: elvish - directory history [(more)](learn/tour.html#directory-history) home/dir-history ``` Press Ctrl-R for command history. That beautiful `ffmpeg` command you crafted two months ago is still there. ```ttyshot Terminal: elvish - command history [(more)](learn/tour.html#command-history) home/cmd-history ``` Press Ctrl-N for the builtin file manager. Explore directories and files without leaving the comfort of your shell. ```ttyshot Terminal: elvish - file manager [(more)](learn/tour.html#navigation-mode) home/file-manager ```
    Talk with the community
    - Join the [Forum](https://bbs.elv.sh) to ask questions, share your experience, and show off your projects! - Join the chatroom to talk to fellow users in real time! The following channels are all bridged together thanks to [Matrix](https://matrix.org): - Telegram: [Elvish user group](https://t.me/+Pv5ZYgTXD-YaKwcP) - Discord: [Elvish Shell](https://discord.gg/jrmuzRBU8D) - Matrix: [#users:elv.sh](https://matrix.to/#/#users:elv.sh) - IRC: [#elvish](https://web.libera.chat/#elvish) on Libera Chat - Gitter: [elves/elvish](https://gitter.im/elves/elvish)
    More resources
    - [Try Elvish](https://try.elv.sh) directly from the browser (beta) - [Awesome Elvish](https://github.com/elves/awesome-elvish): Official list of unofficial Elvish modules - [@ElvishShell](https://twitter.com/elvishshell) on Twitter
    elvish-0.21.0/website/home/000077500000000000000000000000001465720375400154735ustar00rootroot00000000000000elvish-0.21.0/website/home/cmd-history-ttyshot.elvts000066400000000000000000000005031465720375400225260ustar00rootroot00000000000000~> use store store:add-cmd 'cd ~/videos' store:add-cmd 'ffmpeg -i input.mp4 -c:v libx264 -c:a aac output.mp4' store:add-cmd 'ffmpeg -i input.mp4 -vf "transpose=1,scale=640:360,split [a][b];[a] palettegen [p];[b][p] paletteuse" -loop 0 output.gif' edit:history:fast-forward ~> echo '[CUT]' ~> #send-keys C-R ff elvish-0.21.0/website/home/cmd-history-ttyshot.html000066400000000000000000000005571465720375400223460ustar00rootroot00000000000000~> elf@host HISTORY (dedup on) ff Ctrl-D dedup 34 ffmpeg -i input.mp4 -c:v libx264 -c:a aac outpu 35 ffmpeg -i input.mp4 -vf "transpose=1,scale=640: elvish-0.21.0/website/home/dir-history-ttyshot.elvts000066400000000000000000000002511465720375400225410ustar00rootroot00000000000000~> use store store:add-dir /opt/java store:add-dir ~/java/com/acme/project store:add-dir ~/java/com/acme/project/utilities echo '[CUT]' ~> #send-keys C-L j elvish-0.21.0/website/home/dir-history-ttyshot.html000066400000000000000000000004271465720375400223550ustar00rootroot00000000000000~> elf@host LOCATION j 10 ~/java/com/acme/project/utilities 10 ~/java/com/acme/project 10 /opt/java elvish-0.21.0/website/home/file-manager-ttyshot.elvts000066400000000000000000000001141465720375400226110ustar00rootroot00000000000000~> cd elvish set edit:max-height = 10 echo '[CUT]' ~> #send-keys C-N elvish-0.21.0/website/home/file-manager-ttyshot.html000066400000000000000000000016041465720375400224250ustar00rootroot00000000000000~/elvish> elf@host NAVIGATING Ctrl-H hidden Ctrl-F filter bash 1.0-release.m 1.0 has not been released y elvis CONTRIBUTING. zsh Dockerfile LICENSE Makefile PACKAGING.md README.md SECURITY.md elvish-0.21.0/website/home/home.css000066400000000000000000000070731465720375400171440ustar00rootroot00000000000000:root { --ttyshot-width: 450px; } /* Cancel the global width restriction. Content on the homepage still respect the 800px width, but they may have different background colors that span the entire width. */ #main { max-width: none; padding: 0; } /* Intro layout. */ .intro { margin: auto; display: flex; flex-direction: column; /* Replicate the width restriction of #main. */ max-width: 832px; /* Replicate the padding of #main, and add some padding-bottom. */ padding: 0 16px 32px; } .intro .intro-content { padding-bottom: 8px; } .intro .action { display: flex; column-gap: 16px; flex-wrap: wrap; gap: 16px; } .intro .action a { width: fit-content; height: 40px; padding: 0 16px; border: 1px solid; border-radius: 5px; font-family: var(--sans-font); color: black; display: flex; justify-content: center; align-items: center; } .dark .intro .action a { color: white; } .intro .action a.primary { border-color: green; background-color: green; color: white; box-shadow: 0 0.125rem 0.3125rem 0 rgba(0, 0, 0, 0.2); } .button.community::before { font-family: "fa-elv-sh"; content: var(--icon-chat); font-size: 0.9em; font-weight: normal; margin-top: -0.1em; padding-right: 0.3em; /* color: var(--deep-orange-500); */ } /* Section layout. */ section { display: flex; flex-direction: column; align-items: center; padding-top: 20px; /* Sections usually end with a block that already has some margin-bottom, so only add a small amount. */ padding-bottom: 12px; } section:nth-child(even) { background-color: #f0f0f0; } .dark section:nth-child(even) { background-color: #252525; } /* Replicate the width restriction of #main on the children of
    . The
    itself may need to have a gray background, so must be full width. */ section > header, section > .content { /* We want to left-justify the text in
    , so we need to set an explicit width. Using "max-width: 832px" causes the text to be centered. */ width: min(832px, 100%); padding: 0 16px; } section > header, .column > header { font-family: var(--sans-font); font-size: 1.3em; font-weight: bold; } .content { /* When the screen is wide enough, .showcase.content has pairs of

    and

     side by side. To simplify the markup, instead of grouping each pair
         in a container, we rely on flex wrapping to achieve the desired layout.
         
         .columns.content has pairs of .column side by side when the screen is wide
         enough. */
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
    }
    
    .showcase.content > p {
      /* The extra 20px becomes the gap between 

    and

     due to
         "justify-content: space-between". */
      width: calc(800px - var(--ttyshot-width) - 20px);
    }
    
    .showcase.content > pre {
      /* Ttyshots need to have a fixed width, so give all 
    s the same width
         according to ttyshots. */
      width: var(--ttyshot-width);
    }
    
    .column {
      width: 400px;
    }
    
    /* The side-by-side arrangement of 

    and

     pairs in each .showcase.content
       assume a total width of 800px. When the content is no longer that wide,
       arrange them vertically instead. */
    @media screen and (max-width: 831px) {
      /* Make the width of 

    100% and flex-flow will take care of the layout change. Also make non-ttyshot blocks 100% to look a bit nicer. */ .showcase.content > p, .showcase.content > pre:not(.language-ttyshot), .columns.content > .column { width: 100%; } /* This is somewhat hacky. Ideally we should also give it a gray background. */ .column:nth-child(even) { padding-top: 8px; } } elvish-0.21.0/website/home/pipelines-ttyshot.elvts000066400000000000000000000002111465720375400222500ustar00rootroot00000000000000~> range 1100 1111 | each {|x| curl -sL xkcd.com/$x/info.0.json } | from-json | each {|x| printf "%g: %s\n" $x[num] $x[title] } elvish-0.21.0/website/home/pipelines-ttyshot.html000066400000000000000000000020031465720375400220600ustar00rootroot00000000000000~> range 1100 1111 | each {|x| curl -sL xkcd.com/$x/info.0.json } | from-json | each {|x| printf "%g: %s\n" $x[num] $x[title] } 1100: Vows 1101: Sketchiness 1102: Fastest-Growing 1103: Nine 1104: Feathers 1105: License Plate 1106: ADD 1107: Sports Cheat Sheet 1108: Cautionary Ghost 1109: Refrigerator 1110: Click and Drag elvish-0.21.0/website/icon-font.css000066400000000000000000000070321465720375400171530ustar00rootroot00000000000000/* Generated with https://fontello.com: - Disable hinting - Include: - icon-doc-text - icon-file-code - icon-heart - icon-link - icon-link-ext - icon-terminal - icon-chat - Copy the base64-encoded WOFF from css/*-embedded.css - Copy the codes from css/*-codes.css All icons are from Font Awesome 4.7.0 (license: SIL) */ @font-face { font-family: 'fa-elv-sh'; src: url('data:application/octet-stream;base64,d09GRgABAAAAAAh0AAsAAAAADPQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAARAAAAGA+I1PmY21hcAAAAYgAAACFAAAB5K3jqkdnbHlmAAACEAAABAAAAAW0n8ac72hlYWQAAAYQAAAAMAAAADYov4GNaGhlYQAABkAAAAAdAAAAJAc9A1pobXR4AAAGYAAAABcAAAAgHZIAAGxvY2EAAAZ4AAAAEgAAABIGQASQbWF4cAAABowAAAAfAAAAIAEYAGluYW1lAAAGrAAAAXUAAALNzZ0bHHBvc3QAAAgkAAAAUAAAAGqbx7bkeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGHexDiBgZWBgamKaQ8DA0MPhGZ8wGDIyAQUZWBlZsAKAtJcUxgOvGD4eJI56H8WQxTzGoZpQGFGFEVMAKB1DZB4nO2RwQ3DIBAEB0xMZPnhElxAakkVriKvVOC+3AlP+Pjr7AGKUkQODdKu0CHtAjdgEA8RwL1x2LzkuuoPTNUPPKVn7nhC8nnPZ1nLcV2Q+FXfcXq99GPKa1fQjyNRcuQ/c723rqIl2bDsU0e5kTrWVd4b1lc+G9ZjWRvKl3I0iB8I0ydiAAAAeJyNVE1sG0UUnrezO7Nem921vZ6NUteJ7dhbhXTXXm/Wh1InimkoIS2JS0EFlEMPPQQOSLEIEjTqIUpShNILEkgcEFKLKnHoj3LiiuAc9ZADya2XcA8HRLLwvE5ohZDorrTzZua9p+/73ntLgJC/HtF9miJ5QpSSA45Lx4Ow6Rcku0VtX1gcGBzoyeiuoY2WBjdvli623Kw1Njldvrl5K7qnvanBnKGF4Vzl089hYLSUs0bODMIXv9+KHmmEUMz/I/2avo75z5H3CakEHlRL3C7AEFiMl1zwgFlDIPwJCCG+NI4Pmo4rIRYEgbshvLXwpoQeQdPprxji453F7XilZ82u6ZqCpkAaFqquyyJrLhlnzY4ZLRtL5hgaZtdw0zbV5IKd0HlSVrUMHNQ67rq75NbrtXWv63kdb837Z/eDbXTNjKAmMxC0rMki7Rpdw5g34VthLplGxxjDpJhT1xNiGGRFpYlkOtqc8ua92pK3XqvXMc2a23G9rrvR3xEJdXlI96WfSIW8QQhYOpRLDuOMl0vV8WoQjk9IIW+2pKYLTtACvwA2ytVTTQxRi+vgVPFlGCQafthsoZdTAJpZ3Pqg5k9fzZ2SGKoAMqWQV1CJC5dhcWtna3Fn7lUlpZ5KyIpMQdJ43np72q999dFw+t27Uxdm4aWLb8G9S6uzibqtyBoHWQbKMElBGciYZ7rty6uzs6t77Y8dXWjFJFWYBpKqIWu7npihvheszbw86nxC8Olx/AN7q0McMoEccxYrF5FdGvur6IvTEO8ZT1vCLvrheQjCc+CLbBrF8OCk1Eg0CBvoTzML7ajWXlhow22uaTxargQQjsB2JdDUEVXbEfnk9ehLxZQnGYMPryeFDqcNC2YfxzHbUwsQ+wWVqBZHPlY1DX6NnhqWlMJAxiYVPQ7MC9HDzxD/Ir2G+LNkkJRJg7TJFeRRgFwaa4FUnGMqNs81qog2m8bqOePFdLYP+ZhidVx5ZvdoNv69pw1TFOyjDHbYsIADERozTxT+gP2yjyfRlTuaCrcRrRoto9bfnFjPn2qqtGIGA0crcQr6GS7CrD5Jswf88KH0ynDut8Pv4WlPt5Ge87VYivhz55l5UrPvpHlikRFCEnAyaE5/2myctpNCxsiRJbwW/YnK5Rnb3WUsjwUARZGuaurRfoxrADG76KLkmc529/qu0aFiRoflnkPv8196N3Ey3ntBvfv/AQNswY5/Es2iMKDXSC700IeIXtjUhWa2hUNUAE5xhv5H959lU8lzeuOGLCFmXVlRVem8qq7IOs8zST4+N+keV+ESpNRyf+UvUIoDTnOoxMYGps3J/H4Sn/tMUp47lFj0DmBHbmNWRUrxqIYGIX8DALf8MHicY2BkYGAA4o7fx9Xi+W2+MnAzvwCKMDy6I/cNQf/PYn7BHATkcjAwgUQBgPMNX3icY2BkYGAO+p8FJF8wMPz/DySBIiiAAwCH0QWdAAAAeJxjfsHAwAzCC6A0CEdC+UAaAGzOBasAAAAAAAAwALwBLgGOAggCRgLaAAB4nGNgZGBg4GCIZWBjAAEmIOYCQgaG/2A+AwASwQGCAHicdZDLasJAFIb/8dKLQlta6LazKkpp1GA3giBYdNNupLgtMcYkEjMyGQVfo+/Qh+lL9Fn6G8dSlCZM5jvfnDkzOQCu8Q2B3fPEsWOBM0Y7LuAUXctF+mfLJfKL5TKqeLN8Qv9uuYIHhJaruMEHK4jSOaM5Pi0LXIlLywVciDvLRfpHyyVy13IZt+LV8gm9b7mCscgsV3EvvvpqudFxGBlZ69el23TbcrKRiipOvUR6KxMpncmenKnUBEmiHF8t9jwKwlXi6X24n8eBzmKVypbT3KthkAbaM8F0Wz1bh64xMznTaiEHNkMutZoHvnEiY5adRuPveehDYYkNNGK2KoKBRI22ztlFk6NNmjBDMnOXFSOFh4TGw4o7onwlY9zjmDFKaQNmJGQHPr+LIz8ihdyfsIo+Wj2Mx6TtGXHuJVqs2zzKGpLSPNPL7fT37hnWPM2lNdy1vaXObyUxOKgh2Y/t2pzGp3fyrhjaDhp8//m/H+BnhE0AAAB4nG3GQQqAIBAF0PmmNXYXDyU2oWQKMouOH9K2t3pk6LPTP4bBAguHFRsYnlyWONTW0i6bclSeC/IoHz0FnVEZd2mx+rNUCakfQvQCJCUS8Q==') format('woff'); } :root { /* Codepoints for Font Awesome icons */ --icon-heart: '\e800'; --icon-link: '\e801'; --icon-chat: '\e802'; --icon-link-ext: "\f08e"; --icon-doc-text: "\f0f6"; --icon-file-code: "\f1c9"; --icon-terminal: "\f120"; } elvish-0.21.0/website/index.toml000066400000000000000000000010551465720375400165500ustar00rootroot00000000000000title = "Elvish Shell" author = "Qi Xiao" feedPosts = 10 template = "template.html" baseCSS = ["reset.css", "style.css", "icon-font.css", "sgr.css"] rootURL = "https://elv.sh" [index] name = "home" extraCSS = ["home/home.css"] [[categories]] name = "get" title = "Get Elvish" [[categories]] name = "learn" title = "Learn Elvish" [[categories]] name = "ref" title = "Elvish Reference" [[categories]] name = "blog" title = "Elvish Blog" [[categories]] name = "sponsor" title = "Sponsoring Elvish's Development" navHTML = "" elvish-0.21.0/website/learn/000077500000000000000000000000001465720375400156445ustar00rootroot00000000000000elvish-0.21.0/website/learn/arguments-and-outputs.md000066400000000000000000000176111465720375400224620ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - [Your first Elvish commands](first-commands.html) - **Arguments and outputs** - [Variables and loops](variables-and-loops.html) - [Pipelines and IO](pipelines-and-io.html) - [Value types](value-types.html) - [Organizing and reusing code](organizing-and-reusing-code.html) # Arguments and quoting Let's take a closer look at some of the commands we've run: ```elvish-transcript Terminal - elvish ~> + 2 10 ▶ (num 12) ~> echo Hello, world! Hello, world! ``` In the first command, `+` is given two arguments, `2` and `10`, separated by spaces -- so far so good. In the second command, `echo` is given `Hello, world!`. Following how we read the first command, this should also be *two* arguments. But this still achieved what we want -- printing out `Hello, World!` -- but how? As it turns out, what's happening with this simple command is actually not so simple: 1. Elvish recognizes `Hello, World!` as two arguments and passes them to `echo`. 2. The `echo` command receives two arguments and it joins them with a space before printing. The consequence of this process can be better observed when you'd like to print multiple spaces: ```elvish-transcript Terminal - elvish ~> echo Hello, world! Hello, world! ``` Although we've used three spaces, all `echo` receives is two arguments `Hello,` and `world!`, so all it can do is joining them back with a single space. ## Quoting In order to pass spaces to `echo`, we can pass a **quoted** argument: ```elvish-transcript Terminal - elvish ~> echo 'Hello, world!' Hello, world! ``` A pair of single quotes delimits a **quoted string**, and tells Elvish that the text inside it is a single argument. In this case, `echo` only sees one argument containing Hello,   world (with three spaces). You can use quoted arguments wherever you can use an unquoted argument. In fact, you can even quote the command name itself. This applies to all the commands we have seen so far: ```elvish-transcript Terminal - elvish ~> '+' '2' '10' ▶ (num 12) ~> 'randint' '1' '7' ▶ (num 3) ``` However, writing commands like this is unnecessary and unreadable. It's better to reserve quoting to situations when it's necessary, for example when the argument you'd like to pass has spaces. ## Metacharacters Another common reason to use quoting is when your argument includes **metacharacters**, characters with special meaning to Elvish. Some examples of metacharacters are `#`, which introduces a comment, and `(` and `)`, which we'll encounter soon. Quoting them stops Elvish from treating them as metacharacters: ```elvish-transcript Terminal - elvish ~> echo '(1) Rule #1' (1) Rule #1 ``` We will learn more metacharacters as we learn more of Elvish's syntax. As a rule of thumb, punctuation marks in the [ASCII range](https://en.wikipedia.org/wiki/ASCII#Character_set) tend to be metacharacters, except those commonly found in file paths, like `_`, `-`, `.`, `/` and `\`. ## Double quotes You can also create quoted strings with double quotes, like `"foo"` rather than `'foo'`. It's useful when the string itself contains single quotes: ```elvish-transcript Terminal - elvish ~> echo "It's a wonderful world!" It's a wonderful world! ``` Another difference is that double quotes allow you to write special characters using **escape sequences** that start with `\`. For example, `\n` inside double quotes represents a newline: ```elvish-transcript Terminal - elvish ~> echo "old pond\nfrog leaps in\nwater's sound" old pond frog leaps in water's sound ``` You can read more about [single-quoted strings](../ref/language.html#single-quoted-string) and [double-quoted strings](../ref/language.html#double-quoted-string) in the language reference page. # Working with command outputs Let's go back to our arithmetic examples: ```elvish-transcript Terminal - elvish ~> + 2 10 # addition ▶ (num 12) ~> * 2 10 # multiplication ▶ (num 20) ``` What if we want to calculate something that involves multiple operations, like `3 * (2 + 10)`? Of course, we can simply run multiple commands: ```elvish-transcript Terminal - elvish ~> + 2 10 ▶ (num 12) ~> * 3 12 ▶ (num 36) ``` But this makes Elvish a really clumsy calculator! Instead, we can **capture** the output of the `+` command by placing it inside `()`, and use it as an argument to the `*` command: ```elvish-transcript Terminal - elvish ~> * (+ 2 10) 3 ▶ (num 36) ``` Now is also the time to explain the `▶` notation. It indicates that the output is a **value** in Elvish's data type system. In this case, `(num ...)` is a **typed number**, although for the purpose of passing to `*`, `(num 12)` and `12` work identically. We will learn more in [Value types](value-types.html). ## Concatenating results Other than using the output of a command as an argument on its own, you can also concatenate it to something else to build a bigger argument. For example, if we want to add some message explaining what the result is: ```elvish-transcript Terminal - elvish ~> echo 'And the result is... '(* (+ 2 10) 3) And the result is... 36 ``` (For the purpose of string concatenation, `(num 36)` becomes just `36`.) When the output of a command doesn't start with `▶`, it indicates that the output is a stream of *bytes*. For example, `echo` produces bytes: ```elvish-transcript Terminal - elvish ~> echo Hello! Hello! ``` Among Elvish's builtin commands, some output values, while some output bytes. On the other hand, external commands can only output bytes; they don't have direct access to Elvish's system of data types. Let's finish this section by augmenting our "Hello, World!" example with the output of a useful external commands: ```elvish-transcript Terminal - elvish ~> echo 'Hello World! My name is: '(whoami) Hello World! My name is: elf ``` # Command history We have now worked with quite a few commands, some more simple, some more complex. Inevitably, you'll want to run some commands that are either the same or similar to something you have run in the past. This is where Elvish's **command history** feature is useful. For example, if we rolled the dice once with `randint 1 7` and want to roll it again, we can press : ```ttyshot Terminal - elvish learn/arguments-and-outputs/command-history-up ``` We are now in a **history walking** mode. The basic operations work as follows: - Press to go back further, or to go forward. - If you can't find your command, press Esc to exit this mode cleanly. - Pressing any other key accepts the current entry and do what the key usually does. For example, simply pressing Enter will accept the entry and run it. If you want to edit the command before running it, just start editing by pressing Backspace, Ctrl-W, and so on. Walking the history like this is the best option if the command is recent. To find a more distant command, you can use the **history listing** mode instead by pressing Ctrl-R: ```ttyshot Terminal - elvish learn/arguments-and-outputs/command-history-listing ``` This mode works very similarly to **location mode** we saw in [Your first Elvish commands](first-commands.html). You can use and to select an item, and press Enter to insert to it. Similarly, you can type part of the command to narrow down the list: ```ttyshot Terminal - elvish learn/arguments-and-outputs/command-history-listing-narrowed ``` # Conclusion In this part, we dived into the inner workings of arguments and quoting, and learned how to capture and use the output of commands, and the distinction between *value output* and *byte output*. We also learned how to recall command history. These skills will help you build and use more complex commands and use them with ease. Let's now move on to the next part, [Variables and loops](variables-and-loops.html). elvish-0.21.0/website/learn/arguments-and-outputs/000077500000000000000000000000001465720375400221325ustar00rootroot00000000000000elvish-0.21.0/website/learn/arguments-and-outputs/command-history-listing-narrowed-ttyshot.elvts000066400000000000000000000001541465720375400333300ustar00rootroot00000000000000//rows 8 ~> store:add-cmd 'randint 1 7' edit:history:fast-forward echo '[CUT]' ~> #send-keys C-R git elvish-0.21.0/website/learn/arguments-and-outputs/command-history-listing-narrowed-ttyshot.html000066400000000000000000000005561465720375400331450ustar00rootroot00000000000000~> elf@host HISTORY (dedup on) git Ctrl-D dedup 7 git branch 8 git checkout . 9 git commit 19 git status elvish-0.21.0/website/learn/arguments-and-outputs/command-history-listing-ttyshot.elvts000066400000000000000000000001501465720375400315050ustar00rootroot00000000000000//rows 8 ~> store:add-cmd 'randint 1 7' edit:history:fast-forward echo '[CUT]' ~> #send-keys C-R elvish-0.21.0/website/learn/arguments-and-outputs/command-history-listing-ttyshot.html000066400000000000000000000012601465720375400313170ustar00rootroot00000000000000~> elf@host HISTORY (dedup on) Ctrl-D dedup 21 echo $pwd 22 * (+ 3 4) (- 100 94) 31 make 32 math:min 3 1 30 33 randint 1 7 elvish-0.21.0/website/learn/arguments-and-outputs/command-history-up-ttyshot.elvts000066400000000000000000000001351465720375400304630ustar00rootroot00000000000000~> store:add-cmd 'randint 1 7' edit:history:fast-forward echo '[CUT]' ~> #send-keys Up elvish-0.21.0/website/learn/arguments-and-outputs/command-history-up-ttyshot.html000066400000000000000000000003211465720375400302670ustar00rootroot00000000000000~> randint 1 7 elf@host HISTORY #34 elvish-0.21.0/website/learn/effective-elvish.md000066400000000000000000000344031465720375400214220ustar00rootroot00000000000000 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 arbitrary Elvish values. The most fundamental command that does this is `put`: ```elvish-transcript ~> put foo ▶ foo ~> var 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] ~> var 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 ~> var 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 ~> var 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 ~> var 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`. 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 ~> var 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 ~> var 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 ~> var 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 ~> var li = [(str:split , a,b,c)] ~> put $li ▶ [a b c] ~> var @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.21.0/website/learn/faq.md000066400000000000000000000055031465720375400167400ustar00rootroot00000000000000 # 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 [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](https://en.wikipedia.org/wiki/Elvish_languages_(Middle-earth)), 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.21.0/website/learn/first-commands.md000066400000000000000000000300771465720375400211230ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - **Your first Elvish commands** - [Arguments and outputs](arguments-and-outputs.html) - [Variables and loops](variables-and-loops.html) - [Pipelines and IO](pipelines-and-io.html) - [Value types](value-types.html) - [Organizing and reusing code](organizing-and-reusing-code.html) # Series introduction Welcome to Elvish! This series of articles will teach you the basics of Elvish, covering both its programming language and interactive features. We will be working through practical examples and see how Elvish can help you unlock your productivity at the command line. You don't need to be experienced in other shells, but you need to be familiar with basic operating system concepts like file systems and programs. Before you start, make sure that you have [installed Elvish](../get/). After that, type `elvish` in the terminal and press Enter to start Elvish (`$` represents the existing shell's prompt and is not part of what you type): ```sh-transcript Terminal $ elvish ``` All the examples below assume that you have started Elvish. Alternatively, you can [use Elvish as your default shell](../get/default-shell.html) and have Elvish launched by default. # Hello World! Let's begin with the classical ritual of printing ["Hello, World!"](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program): ```elvish-transcript Terminal - elvish ~> echo Hello, world! Hello, world! ``` You can follow this example (and many others) by typing the code and pressing Enter. The `~>` part is Elvish's prompt; you don't need to type it. Elvish code often follows a "**command** + **arguments**" structure. In this case, the command is [`echo`](../ref/builtin.html#echo), whose job is to just print out its arguments. # Builtin commands The `echo` command does a very simple job, but that's just one of many commands Elvish supports. Running other commands follows a similar "command + arguments" structure. For instance, the [`randint`](../ref/builtin.html#randint) command takes two arguments *a* and *b* and generates a random integer from the set {*a*, *a*+1, ..., *b*-1}. You can use it as a digital dice: ```elvish-transcript Terminal - elvish ~> randint 1 7 ▶ (num 3) ``` (We'll get to the `▶` notation later in this article, and the `(num 3)` notation in [Value types](value-types.html).) Arithmetic operations are also commands. Like the `echo` and `randint` commands we have seen, the command names comes first, making the syntax a bit different from common mathematical notations (sometimes known as [Polish notation](https://en.wikipedia.org/wiki/Polish_notation)): ```elvish-transcript Terminal - elvish ~> + 2 10 # addition ▶ (num 12) ~> * 2 10 # multiplication ▶ (num 20) ``` These commands come with Elvish and are thus called **builtin commands**. The reference page for the [builtin module](../ref/builtin.html) contains all the builtin commands you can use directly. ## Commands in modules Some builtin commands live in **modules** that you need to **import** first. For example, mathematical operations such as the power function is provided by the [`math` module](../ref/math.html): ```elvish-transcript Terminal - elvish ~> use math # import the "math" module ~> math:pow 2 10 # raise 2 to the power of 10 ▶ (num 1024) ``` We'll learn more about modules in [Organizing and reusing code](organizing-and-reusing-code.html). ## Comment syntax You may have noticed that we have annotated some of our commands above with texts like `# texts`: ```elvish-transcript Terminal - elvish ~> + 2 10 # addition ▶ (num 12) ``` The `#` character marks the rest of the line as a **comment**, which is ignored by Elvish. When typing out the examples, you can include the comments or leave them out. # External commands While Elvish provides a lot of useful functionalities as builtin commands, it can't do everything. This is where **external commands** come in, which are separate programs installed on your machine. Many useful programs come in the form of external commands, and there is no limit on what they can do. Here are just a few examples: - [Git](https://git-scm.com) provides the `git` command to manage code repositories - [Pandoc](http://pandoc.org) provides the `pandoc` command to convert document formats - [GraphicsMagick](http://www.graphicsmagick.org) provides the `gm` command, and [ImageImagick](https://www.imagemagick.org/script/index.php) provides the `magick` command to process images - [FFmpeg](http://ffmpeg.org) provides the `ffmpeg` command to process and videos - [Curl](https://curl.se) provides the `curl` command to make HTTP requests - [Nmap](https://nmap.org) provides the `nmap` command to test the security of websites - [Tcpdump](http://www.tcpdump.org) provides the `tcpdump` command to analyze network traffic - Elvish itself can be used as an external command, as `elvish` - Your operating system comes with many of external commands pre-installed. For example, Unix systems provide [`ls`](https://en.wikipedia.org/wiki/Ls) for listing files, and [`cat`](https://en.wikipedia.org/wiki/Cat_(Unix)) for showing files. Both Unix systems and Windows provide [`ping`](https://en.wikipedia.org/wiki/Ping_(networking_utility)) for testing network connectivity. These example are all command-line programs, but even graphical programs often provide command-line interfaces, which can give you access to advanced configuration options. For example, you can invoke [Chromium](https://www.chromium.org/developers/how-tos/run-chromium-with-flags/) like `chromium --js-flags=...` to customize internal JavaScript options (the exact command name depending on the operating system). ## A concrete example Much of the power of shells comes from the ease of running external commands, and Elvish is no exception. Here is an example of how you can download Elvish, using a combination of external commands: - `curl` to download files over HTTP - `tar` to unpack archive files - `shasum` (on Unix systems) or `certutil` (on Windows) to calculate the [SHA-256 checksum](https://en.wikipedia.org/wiki/SHA-2) of files ```elvish-transcript Terminal - elvish ~> curl -s -o elvish-HEAD.tar.gz https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz ~> curl -s https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz.sha256sum 93b206f7a5b7f807f6b2b2b99dd4074ed678620541f6e9742148fede0a5fefdb elvish-HEAD.tar.gz ~> shasum -a 256 elvish-HEAD.tar.gz # On a Unix system 93b206f7a5b7f807f6b2b2b99dd4074ed678620541f6e9742148fede0a5fefdb elvish-HEAD.tar.gz ~> certutil -hashfile elvish-HEAD.tar.gz SHA256 # On Windows 93b206f7a5b7f807f6b2b2b99dd4074ed678620541f6e9742148fede0a5fefdb ~> tar -xvf elvish-HEAD.tar.gz x elvish ~> ./elvish ``` (Note: To install Elvish, you're recommended to use the script generated on the [Get Elvish](../get/) page instead. This example is for illustration, and assumes your OS to be Linux and CPU to be Intel 64-bit. We will see how to avoid making these assumptions in [Variables and loops](variables-and-loops.html).) Running external commands follow the same "command + arguments" structure. For example, in the first `curl` command, the arguments are `-s`, `-o`, `elvish-HEAD.tar.gz` and `https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz` respectively. External commands often assign special meanings to arguments starting with `-`, sometimes called **flags**. In the case of `curl`, the arguments work as follows: - `-s` means "silent mode". You can try leaving this argument out to see more output from `curl`. - `-o`, along with the next argument `elvish-HEAD.tar.gz`, means that `curl` should output the result to `elvish-HEAD.tar.gz`. - `https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz` is the URL to request. The second `curl` command leaves out the `-o` and its next argument, directing `curl` to write the result directly as output. This allows us to see the checksum directly in the terminal. You can find out what flags each external command accepts in their respective manual. You'll also find commands that accept flags starting with `--`, and on Windows, starting with `/`. # The working directory So far, we have run all the commands from the prompt `~>`. The `>` part only functions as a visual clue, but the `~` part indicates the [**working directory**](https://en.wikipedia.org/wiki/Working_directory) of Elvish. In this case, the special `~` indicates your home directory, which can be /home/*username*, /Users/*username*, or C:\\Users\\*username*, depending on the operating system. Commands that work with files are affected by the working directory, especially when you use a [**relative path**](https://en.wikipedia.org/wiki/Path_(computing)#Absolute_and_relative_paths) like `foo` (as opposed to an absolute path like `/etc/foo` or `C:\foo`). Let's look at our last example more carefully: - The first `curl` command writes `elvish-HEAD.tar.gz` to the working directory. - The `shasum` or `certutil` command reads `elvish-HEAD.tar.gz` from the working directory. - The `tar` command reads `elvish-HEAD.tar.gz` from the working directory, and writes `elvish` to the working directory. You can change the working directory with the [`cd`](../ref/builtin.html#cd) command, and all these commands will work with files in that directory instead. For example, let's create a `tmp` directory under the home directory and use that as our working directory instead: ```elvish-transcript Terminal - elvish ~> mkdir tmp # on Unix ~> md tmp # on Windows ~> cd tmp ~/tmp> # Now run more commands... ``` You'll notice that Elvish's prompt has changed to `~/tmp` (or `~\tmp` on Windows) to reflect the new working directory. Our examples will continue to use the home directory for simplicity, but it's recommended that you try them in a separate directory and keep the home directory clean. ## Directory history As you work with different directories on your filesystem, you'll find yourself using `cd` very frequently to jump back and forth between directories, and this can become quite a chore. Elvish actually remembers all the directories you have been to, and you can access that history with the **location mode** by pressing Ctrl-L: ```ttyshot Terminal - elvish learn/first-commands/location-mode ``` Press and to select a directory, and press Enter to use it as your working directory. You can also just type part of the path to narrow down the list. For example, to only see paths that contain `elv`: ```ttyshot Terminal - elvish learn/first-commands/location-mode-elv ``` # Editing the code interactively If you have tried all the examples on your computer, it's possible that you have made some typos. (If you haven't made any, pause and appreciate your typing skills.) You are likely already familiar with these keys: - ◀︎ and ▶︎ move the cursor one character at a time. - Alt-◀︎ and Alt-▶︎ move the cursor one word at a time. - Home and End move the cursor to the start or end of the line. - Backspace deletes the character to the left of the cursor. - Delete deletes the character on the cursor (or to its right if your cursor is an I-beam). Elvish also supports some keys found in traditional shells: - Ctrl-W deletes the word to the left of the cursor. - Ctrl-U deletes the entire line, up to the cursor. (See [`readline-binding`](../ref/readline-binding.html) if you'd like to use more readline-style bindings.) # Conclusion Being able to run all sorts of commands easily is one of the greatest strengths of shells. When using Elvish interactively, most of your interactions will consist of simple invocations of various commands. In this part, we learned the basics of running builtin and external commands and how they can be affected by the working directory. We also learned how to change the working directory quickly and how to edit your command. We are now ready for the next part, [Arguments and outputs](arguments-and-outputs.html). elvish-0.21.0/website/learn/first-commands/000077500000000000000000000000001465720375400205725ustar00rootroot00000000000000elvish-0.21.0/website/learn/first-commands/location-mode-elv-ttyshot.elvts000066400000000000000000000001241465720375400267000ustar00rootroot00000000000000//rows 8 ~> use store store:add-dir ~/tmp echo '[CUT]' ~> #send-keys C-L elv elvish-0.21.0/website/learn/first-commands/location-mode-elv-ttyshot.html000066400000000000000000000011611465720375400265110ustar00rootroot00000000000000~> elf@host LOCATION elv 10 ~/elvish 10 ~/.local/share/elvish 10 ~/elvish/website 9 ~/.config/elvish 9 ~/elvish/pkg/edit elvish-0.21.0/website/learn/first-commands/location-mode-ttyshot.elvts000066400000000000000000000001201465720375400261100ustar00rootroot00000000000000//rows 8 ~> use store store:add-dir ~/tmp echo '[CUT]' ~> #send-keys C-L elvish-0.21.0/website/learn/first-commands/location-mode-ttyshot.html000066400000000000000000000011411465720375400257230ustar00rootroot00000000000000~> elf@host LOCATION 18 ~/tmp 10 ~/elvish 10 ~/.local/share/elvish 10 ~/elvish/website 9 ~/.config/elvish elvish-0.21.0/website/learn/index.toml000066400000000000000000000027271465720375400176600ustar00rootroot00000000000000autoIndex = true [[groups]] intro = "Start here if you have experience with other shells:" [[articles]] name = "tour" title = "Quick tour" note = "familiarize yourself with the language and interactive features" extraCSS = ["tour.css"] group = 0 [[articles]] name = "scripting-case-studies" title = "Scripting case studies" note = 'short real-world Elvish scripts (New!)' group = 0 [[groups]] intro = """ Beginner's Guide to Elvish (New!) is for you if you haven’t used shells a lot or want to brush up on the basics: """ [[articles]] name = "first-commands" title = "Your first Elvish commands" group = 1 [[articles]] name = "arguments-and-outputs" title = "Arguments and outputs" group = 1 [[articles]] name = "variables-and-loops" title = "Variables and loops" group = 1 [[articles]] name = "pipelines-and-io" title = "Pipelines and IO" group = 1 [[articles]] name = "value-types" title = "Value types" group = 1 [[articles]] name = "organizing-and-reusing-code" title = "Organizing and reusing code" group = 1 #[[articles]] #name = "fundamentals" #title = "Fundamentals" #note = "if you are new to shells and programming" #group = 1 [[groups]] intro = "Advanced topics:" [[articles]] name = "unique-semantics" title = "Unique semantics" group = 2 [[articles]] name = "effective-elvish" title = "Effective Elvish" group = 2 [[groups]] intro = "Design and history:" [[articles]] name = "faq" title = "FAQ" group = 3 elvish-0.21.0/website/learn/organizing-and-reusing-code.md000066400000000000000000000263211465720375400234630ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - [Your first Elvish commands](first-commands.html) - [Arguments and outputs](arguments-and-outputs.html) - [Variables and loops](variables-and-loops.html) - [Pipelines and IO](pipelines-and-io.html) - [Value types](value-types.html) - **Organizing and reusing code** # Scripts So far, we have run all our Elvish commands directly at Elvish's prompt. This can be quite convenient: you don't have to open an editor or compile your code, just open up your terminal, type something, and see the results. This is true of shells in general, but Elvish also gives you a powerful programming language capable of processing complex data. Still, from time to time, we'll need to organize our code a bit more formally, for example when we need to send it to a different machine or someone else. We can achieve that by simply putting our code in a file. As an example, let's open an editor and type our first very "Hello, world!" program (albeit with proper quoting): ```elvish hello.elv echo 'Hello, world!' ``` After saving the file as `hello.elv`, you can run it like: ```elvish-transcript Terminal - elvish ~> elvish hello.elv Hello, world! ``` Such file are usually called **scripts**. Elvish scripts use the extension `.elv` by convention; this is not a requirement from Elvish itself, but naming the file with an `.elv` extension communicates to other people that this is an Elvish script, and makes it easier for your editor to detect the file's type. # Functions We have seen a lot of commands, both builtin and external. Elvish also gives you the ability to define your own commands. For example, in [Variables and loops](variables-and-loops.html#for-loops-and-lists), we used `gm` to convert JPG files to AVIF files. If we need to perform this conversion frequently, we can define a **function** that takes the name of a JPG file: ```elvish-transcript Terminal - elvish ~> use str ~> fn jpg-to-avif {|jpg| gm convert $jpg (str:trim-suffix $jpg .jpg).avif } ``` After that, you can use `jpg-to-avif` like any other command: ```elvish-transcript Terminal - elvish ~> jpg-to-avif unicorn.jpg ~> jpg-to-avif banana.jpg ``` The [`fn`](../ref/language.html#fn) command defines a function. Here, the function to define is called `jpg-to-avif`, and the part surrounded by `{` and `}` is its **body**. The body starts with `|jpg|`, and it specifies that the function takes a single argument, which becomes a variable `$jpg`. The `|` that surrounds the argument is the same character we use for pipes, but has a different meaning in this context. In Elvish, we also call builtin commands "builtin functions", but external commands are not called functions. ## rc.elv Defining a function at the prompt only makes it available for the current session. If you open a new terminal, you'll have to define it again. To define the function automatically, you can put it in a special `rc.elv` script, which is evaluated before every interactive Elvish session. The path [depends on the system](https://elv.sh/ref/command.html#rc-file), but by default, it's `~/.config/elvish/rc.elv` on Unix systems and `%RoamingAppData%\elvish\rc.elv` on Windows. We can add the following to `rc.elv` (create it if it doesn't exist yet): ```elvish rc.elv use str fn jpg-to-avif {|jpg| gm convert $jpg (str:trim-suffix $jpg .jpg).avif } ``` ## Functions as aliases The `jpg-to-avif` function does something relatively complex, but even very simple functions can be useful. For example, it's a good idea to upgrade your system packages every day, so you may want to define a dedicated function for it and put that in `rc.elv`: ```elvish rc.elv fn up { brew upgrade } ``` (We are using the [Homebrew](https://brew.sh) package manager as an example; change the exact command according to the package manager your system uses.) Note that we have omitted the list of arguments because this function doesn't take any. It's equivalent to: ```elvish rc.elv fn up {|| brew upgrade } ``` (Due to some quirks in Elvish's syntax, you have to follow the `{` with a whitespace character, such as a space or a newline.) Even though our definition of `up` is quite simple, it can still save us a lot of keystrokes if we upgrade our system frequently. This kind of simple functions are sometimes called **aliases**. Some shells have aliases as a distinct concept from functions, but in Elvish they are the same. Another popular alias among Unix users is `ll` for `ls -l`. We can define it like this: ```elvish rc.elv fn ll { ls -l } ``` Our definition has a defect, however. The `ls` command has two modes of operations: - If you run it without any arguments, it lists the current directory. - Alternatively, you can also give it any numbers of files you are interested in, like this: ```elvish-transcript Terminal - elvish ~> ls -l foo bar [ information about foo and bar ] ``` Our `ll` function only supports the former mode. To fix that, we can let it accept any number of arguments too: ```elvish rc.elv fn ll {|@a| ls -l $@a } ``` The `@` in `@a` causes elvish to collect an arbitrary number of arguments into `$a` as a list. We then use `$@a` to "expand" it back into individual arguments. ## Functions as arguments The function body syntax is not restricted to function definitions. Elvish has **first-class functions**, meaning that you can use functions as arguments to other commands too. (See [Wikipedia](https://en.wikipedia.org/wiki/First-class_function) for the general concept). We have actually seen a few of those. For example, the `each` command takes a function and run it for each of the inputs. ```elvish-transcript Terminal - elvish ~> put 1 2 3 | each {|n| * $n 2} ▶ (num 2) ▶ (num 4) ▶ (num 6) ``` A slightly more subtle occurrence is the body of `for` and `if` commands, which look like `{ commands }`. These are in fact functions that don't take any arguments. # More on scripts Like functions, scripts can also take arguments. Let's try running `hello.elv` with some arguments: ```elvish-transcript Terminal - elvish ~> elvish hello.elv foo bar Hello, world! ``` As you can see, this doesn't change the behavior of our script. That is because we aren't actually using the arguments: unlike functions which declare their arguments inside `||`, arguments to scripts are available implicitly as a list in `$args`. Let's make our scripts treat each argument as someone to say hello to, falling back to `world` if there are no arguments: ```elvish hello.elv if (== 0 (count $args)) { echo 'Hello, world!' } else { for who $args { echo 'Hello, '$who'!' } } ``` Here, the [`==`](../ref/builtin.html#num-eq) command compares two numbers, and the [`count`](../ref/builtin.html#count) command counts the number of elements in a list. We can check that the new `hello.elv` works as intended: ```elvish-transcript Terminal - elvish ~> elvish hello.elv Hello, world! ~> elvish hello.elv Julius Augustus Hello, Julius! Hello, Augustus! ``` One important thing to keep in mind is that the command `elvish hello.elv` behaves like an external command. Even though it's the same program as the Elvish you run it from, it's a separate [process](https://en.wikipedia.org/wiki/Process_(computing)). You can still use Elvish's system of values within the `hello.elv` script itself, but it can't communicate with the "outer world" using Elvish values, only string arguments and byte IO. # Modules In our past exampls, we have often use the following pattern to access additional commands provided by Elvish: ```elvish-transcript Terminal - elvish ~> use str # ① ~> str:trim-suffix a.jpg .jpg # ② ▶ a ``` 1. This command **imports** a **module** to make it available for use. In this case, the module is `str`, and as its abbreviated name suggests, it provides commands for working with strings. 2. To use a command that lives inside a module, we need to prefix it with the module name plus a colon `:`. The technical way to put this is that all the commands in a module lives in a separate **namespace** (see [Wikipedia](https://en.wikipedia.org/wiki/Namespace) for the general concept). (You'll sometimes see the colon treated as part of the module name itself, to make it clear that we are referring to a module; we may say either "the `str` module" or just `str:`.) Elvish has many more builtin modules, and you can see them in the [reference](../ref/) section. Organizing commands into separate modules makes them easier to discover, and the separate namespaces prevent [name collisions](https://en.wikipedia.org/wiki/Name_collision). For example, there is both a [`str:replace`](../ref/str.html#str:replace) command and a [`re:replace`](../ref/re.html#re:replace) command: the former replaces simple literal strings, the latter works with regular expressions. ## Defining and using new modules Just like how you can define your own functions, you can also define your own modules. Do this by placing a file under a module search directory: like `rc.elv`, the path of the directory [depends on the system](../ref/command.html#module-search-directories), but by default, `~/.config/elvish/lib` works on Unix systems and `%RoamingAppData%\elvish\lib` works on Windows. For example, let's collect the `jpg-to-avif` commands into a `img` module, since we may have more of them in future. Create `img.elv` under a module search directory: ```elvish img.elv use str fn jpg-to-avif {|jpg| gm convert $jpg (str:trim-suffix $jpg .jpg).avif } ``` After that, you can use it like this: ```elvish-transcript Terminal - elvish ~> use img ~> img:jpg-to-avif unicorn.jpg ``` Notice that when using a module with `use`, we omit the `.elv` file extension. ## Modules in subdirectories You don't have to put modules directly under a module search directory; you can also store it in a subdirectory. For example, let's collect our `img` module and other modules we have into a `myutils` directory: ```elvish myutils/img.elv use str fn jpg-to-avif {|jpg| gm convert $jpg (str:trim-suffix $jpg .jpg).avif } ``` Then you would use it like this: ```elvish-transcript Terminal - elvish ~> use myutils/img ~> img:jpg-to-avif unicorn.jpg ``` Notice that the `use` command takes the full path to the module (relative to the module search directory), but after that, we'll just use the last part to access it. # Conclusion In this part, we've covered scripts, functions and modules, important mechanisms that allow you to organize code and reuse them. We've also seen how Elvish's support for first-class functions enables commands like `each`, and how Elvish's namespacing mechanism in the module system prevents name conflicts. # Series conclusion Congratulations for finishing the *Beginner's Guide to Elvish* series! We haven't covered everything, but what we have learned should give you a solid basis to build upon, and already allow you to be productive in your daily workflows. You can read more articles in the [learn](./) section, or go directly to [reference manuals](../ref/) (in particular the [language specification](../ref/language.html)). The latter can be a bit dense, but they will give you a complete understanding of how Elvish works, and you should be ready to read them after going through this series. Have fun with Elvish! elvish-0.21.0/website/learn/pipelines-and-io.md000066400000000000000000000251331465720375400213270ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - [Your first Elvish commands](first-commands.html) - [Arguments and outputs](arguments-and-outputs.html) - [Variables and loops](variables-and-loops.html) - **Pipelines and IO** - [Value types](value-types.html) - [Organizing and reusing code](organizing-and-reusing-code.html) # Introduction In the previous parts, we have seen how you can combine commands with *output capture*: you can use the outputs of a command as the arguments to another command. You can also store them in variables or concatenate them with something else. Elvish and other shell languages have another powerful mechanism for combining commands called **pipelines**. But before that, let's examine how input and output of commands work. # IO redirection So far, we have worked with commands that take *arguments* and write *outputs*. Some commands also take **inputs**. As an example, the [`grep`](https://en.wikipedia.org/wiki/Grep) command on Unix systems and the [`findstr`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/findstr) command on Windows reads lines of text, matches each line with a pattern, and writes matched lines as output. This may sound quite complicated, so it's best illustrated with an example. Let's save the following content in a file called `input.txt`: ``` User A: I use Bash User B: I use Elvish User C: I use Zsh User D: I use Elvish ``` After that, let's run the following command to find the lines that contain `Elvish`: ```elvish-transcript Terminal - elvish ~> grep Elvish < input.txt # on Unix systems ~> findstr Elvish < input.txt # on Windows User B: I use Elvish User D: I use Elvish ``` The `< input.txt` syntax is **input redirection** and tells Elvish to use a file as a command's input. Here, we can see `grep` or `findstr` correctly finds the two lines in the file that contains `Elvish`. You can also perform **output redirection** with `>`: ```elvish-transcript Terminal - elvish ~> grep Elvish < input.txt > output.txt # on Unix systems ~> findstr Elvish < input.txt > output.txt # on Windows ``` This command won't have any outputs on the terminal, but if you open `output.txt`, it should contain the same two lines. Input and output are known collectively as **IO**. IO redirection is useful for processing data stored in files and saving results in files. ## Typing inputs from the terminal We can also run commands like `grep` or `findstr` without redirecting their input: ```elvish-transcript Terminal - elvish ~> grep Elvish > output.txt # on Unix systems ~> findstr Elvish > output.txt # on Windows ``` (You can also try running the command without the output redirection, but the interleaving of input and output lines can make things a bit confusing.) After running the command, you'll find your cursor on an empty line without Elvish's `~>` prompt. This is because the `grep` or `findstr` command is still running and waiting for your input. Try typing the same lines that we saved in a file before: ``` User A: I use Bash User B: I use Elvish User C: I use Zsh User D: I use Elvish ``` You also need to indicate that you have finished typing: 1. Make sure that you are on an empty line. If not, press Enter. 2. The next step depends on the operating system: - On Unix systems, press Ctrl-D. - On Windows, press Ctrl-Z and another Enter. If you open up `output.txt`, it should have the two lines containing `Elvish`. Typing inputs directly from the terminal can be quite handy when you have a small amount of input; when you have a lot of input, it's best to store them in a file and use input redirection instead. # Traditional byte pipelines We have seen how we can make `grep` or `findstr` work with files, but another more powerful way to use them is by feeding it the *output* of another command. Here is an example. We have used `curl` to retrieve , an index of files provided by . If we're only interested in the `HEAD` version, we can filter the output of `curl` by connecting it to `grep` or `findstr` using `|`, a **pipe**: ```elvish-transcript Terminal - elvish ~> curl -s https://dl.elv.sh/INDEX | grep HEAD # Unix ~> curl -s https://dl.elv.sh/INDEX | findstr HEAD # Windows https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz.sha256sum https://dl.elv.sh/darwin-arm64/elvish-HEAD.tar.gz https://dl.elv.sh/darwin-arm64/elvish-HEAD.tar.gz.sha256sum ... ``` Like real-world pipelines, you can extend pipelines with more pipes. If we're also only interested in Linux builds, we can add *another* `grep` or `findstr`: ```elvish-transcript Terminal - elvish ~> curl -s https://dl.elv.sh/INDEX | grep HEAD | grep linux # Unix ~> curl -s https://dl.elv.sh/INDEX | findstr HEAD | findstr linux # Windows https://dl.elv.sh/linux-386/elvish-HEAD.tar.gz https://dl.elv.sh/linux-386/elvish-HEAD.tar.gz.sha256sum https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz.sha256sum ... ``` ## Unix's text processing toolkit The `grep`/`findstr` command is just one example of commands that work well in pipelines. In particular, Unix systems, where the idea of pipelines [originate](https://en.wikipedia.org/wiki/Pipeline_(Unix)), provide many more commands designed to work in pipelines. Suppose you are running [Docker](https://en.wikipedia.org/wiki/Docker_(software)) containers and need to read the logs of a container. You can use the `docker` command like this: ```elvish-transcript Terminal - elvish ~> docker logs container-name ... ``` However, this gives you *all* the logs, which you probably don't need. We can use `grep` to only show lines that match a pattern: ```elvish-transcript Terminal - elvish ~> docker logs container-name | grep error ... ``` Or use [`head`](https://en.wikipedia.org/wiki/Head_(Unix)) or [`tail`](https://en.wikipedia.org/wiki/Tail_(Unix)) to only show the first or last N lines: ```elvish-transcript Terminal - elvish ~> docker logs container-name | head -n 20 # read first 20 lines ... ~> docker logs container-name | tail -n 20 # read last 20 lines ... ``` Here's a more elaborate example: the following pipeline shows the top 10 committers of a Git repo, ranked by their number of commits (the output below is from the Git repo of [Go](https://go.googlesource.com/go/)): ```elvish-transcript Terminal - elvish ~/src/go> git log --format=%an | sort | uniq -c | sort -nr | head -n 10 7343 Russ Cox 4303 Robert Griesemer 2992 Rob Pike 2613 Ian Lance Taylor 2377 Brad Fitzpatrick 1734 Austin Clements 1704 Matthew Dempsky 1517 Keith Randall 1496 Josh Bleecher Snyder 1346 Bryan C. Mills ``` In fact, whole books have been written on how to craft Unix pipelines, and they are arguably the original data science toolkit. Windows has also adopted the pipeline mechanism, but it doesn't come pre-installed with the commands we have seen in this section. Nonetheless, you can install them from software sources like [Scoop](https://scoop.sh) and most Unix pipelines will work on Windows too. ## Limitations of byte pipelines As powerful as they are, traditional pipelines do have some pretty severe limitations. As the name "byte pipeline" suggests, the pipes carry streams of bytes, which lack an inherent structure. By *convention*, many tools -- like `grep`, `head` and `tail` -- treat each line as a unit. Some tools also break each line down into space-separated "fields". These conventions give byte pipelines some extra mileage, but they are still limited: you can't easily deal with data that contain newlines or whitespaces themselves, or data with more complex structure, like multiple levels of lists or maps. # Value pipeline To overcome the limitations of traditional byte pipelines, Elvish offers **value pipelines**. We have seen how commands in Elvish can output *values* instead of bytes. For example, the `str:split` splits a string around a separator, and outputs the results as string values: ```elvish-transcript Terminal - elvish ~> str:split , friends,Romans,countrymen ▶ friends ▶ Romans ▶ countrymen ``` There are also commands that can take values as *inputs*. The [`str:join`](../ref/str.html#str:join) command joins multiple strings together, inserting a separator between each adjacent pairs. We can connect the output of `str:split` with the input of `str:join` like this: ```elvish-transcript Terminal - elvish ~> str:split , friends,Romans,countrymen | str:join ' ' ▶ 'friends Romans countrymen' ``` ## Working with both bytes and values The commands we have worked with either use bytes for both input and output (like `grep`/`findstr`) or use values for both input and output (like `str:join`). But that doesn't have to be the case. The [`from-json`](../ref/builtin.html#from-json) command takes byte inputs, parses them as [JSON](https://en.wikipedia.org/wiki/JSON), and writes the result as Elvish values: ```elvish-transcript Terminal - elvish ~> echo '["Julius","Crassus","Pompey"]' | from-json ▶ [Julius Crassus Pompey] ``` (Elvish lists can look a bit like JSON lists. Remember that the leading `▶` indicates value output, and Elvish list elements are separated by spaces rather than commas.) We can also combine pipes and output capture: ```elvish-transcript Terminal - elvish ~> echo '["Julius","Crassus","Pompey"]' | all (from-json) ▶ Julius ▶ Crassus ▶ Pompey ``` It's worth noting that although `from-json` is put in an output capture, it's still able to read the byte inputs from the pipe. It outputs a single list, which is then converted by the `all` to all its elements. There is also a reverse operation of `from-json`: [`to-json`](../ref/builtin.html#to-json) takes Elvish value as inputs, converts them to JSON, and writes the result as bytes: ```elvish-transcript Terminal - elvish ~> put [Julius Crassus Pompey] | to-json ["Julius","Crassus","Pompey"] ``` We haven't quite explored the power of value pipelines. We will see more interesting examples very soon. # Conclusion Elvish allows you to manipulate the byte inputs and outputs of commands with *redirections*, and combine them using *byte pipelines*, a natural and flexible way to express data processing logic. Some details may differ, but byte I/O redirection and pipelines work in other shells too. Elvish infuses pipelines with more power by allowing values to pass through the pipes. This allows you to express data processing logic that involves data with more complex structures, although we've only just had a taste of that. Let's now move on to [Value types](value-types.html) and unlock the full power of value pipelines. elvish-0.21.0/website/learn/scripting-case-studies.md000066400000000000000000000223731465720375400225660ustar00rootroot00000000000000 This page explains the scripting examples on the [homepage](../), illustrating the advantages of Elvish scripting, especially when compared to traditional shells. For more examples of Elvish features compared to traditional shells, see the [quick tour](tour.html). For a complete description of the Elvish language, see the [language reference](../ref/language.html). # jpg-to-png.elv This example on the homepage uses [GraphicsMagick](http://www.graphicsmagick.org) to convert all the `.jpg` files into `.png` files in the current directory: ```elvish jpg-to-png.elv for x [*.jpg] { gm convert $x (str:trim-suffix $x .jpg).png } ``` (If you have [ImageMagick](https://imagemagick.org) installed instead, just replace `gm` with `magick`.) It's equivalent to the following script in traditional shells: ```sh jpg-to-png.sh for x in *.jpg do gm convert "$x" "${x%.jpg}".png done ``` Let's see how the Elvish version compares to the traditional shell version: - You don't need to [double-quote every variable](https://www.shellcheck.net/wiki/SC2086) to prevent unwanted effects. A variable in Elvish always evaluates to one value. - Instead of `${x%.jpg}`, you write `(str:trim-suffix $x .jpg)`. The latter a bit longer, but is easier to remember and understand. Moreover, since `str:trim-suffix` is a normal command rather than a special operator, it's easy to find its documentation - in the [reference doc](../ref/str.html#str:trim-suffix), in the terminal with [`doc:show`](../ref/doc.html#doc:show), or by hovering over it in VS Code (support for more editors will come). - When there is no file that matches `*.jpg`, bash will assign `$x` to the pattern `*.jpg` itself, which is most likely not what you want. Elvish will throw an exception by default, and you can optionally tell Elvish to expand to zero elements with `*[nomatch-ok].jpg`. - Perhaps subjectively, Elvish's syntax is more readable: instead of keywords like `in`, `do` and `done`, Elvish's [`for`](../ref/language.html#for) command doesn't have `in`, and uses familiar punctuation to delimit different parts of the command: the list of elements with `[` and `]`, and the loop body with `{` and `}`. This example doesn't go into the advanced capabilities of Elvish, so differences are small and may seem superficial. However, these small details can quickly add up, and in general it's much easier to develop and maintain scripts in Elvish. # update-servers-in-parallel.elv This example on the homepage shows how you would perform update commands on multiple servers in parallel: ```elvish update-servers-in-parallel.elv var hosts = [[&name=a &cmd='apt update'] [&name=b &cmd='pacman -Syu']] # peach = "parallel each" peach {|h| ssh root@$h[name] $h[cmd] } $hosts ``` Let's break down the script: - The value of `hosts` is a nested data structure: - The outer `[]` denotes a [list](../ref/language.html#list). - The inner `[]` denotes [maps](../ref/language.html#map) - `&k=v` are key-value pairs. So the variable `$hosts` contains a list of maps, each map containing the key `name` and `cmd`, describing a host. - The [`peach`](../ref/builtin.html#peach) command takes: - A [function](../ref/language.html#function), in this case an anonymous one. The signature `|h|` denotes that it takes one argument, and the body is an `ssh` command using fields from `$h`. - A list, in this case `$hosts`. As hinted by the comment, `peach` calls the function for every element of the list in parallel, running these `ssh` commands in parallel. It will also wait for all the functions to finish before it returns. In a real-world script, you'll likely want to redirect the output of the `ssh` command, otherwise the output from the `ssh` commands running in parallel will interleave each other. This can be done with [output redirection](../ref/language.html#redirection), not unlike traditional shells: ```elvish update-servers-in-parallel-v2.elv var hosts = [[&name=a &cmd='apt update'] [&name=b &cmd='pacman -Syu']] peach {|h| ssh root@$h[name] $h[cmd] > ssh-$h[name].log } $hosts ``` With a traditional shell, you can achieve a similar effect with background jobs: ```sh update-servers-in-parallel.sh ssh root@a 'apt update' > ssh-a.log & job_a=$! ssh root@b 'pacman -Syu' > ssh-b.log & job_b=$! wait $job_a $job_b ``` However, you will have to manage the lifecycle of the background jobs explicitly, whereas the [structured](https://en.wikipedia.org/wiki/Structured_concurrency) nature of `peach` makes that unnecessary. Alternatively, you can use external commands such as [GNU Parallel](https://www.gnu.org/software/parallel/) to achieve parallel execution, but this requires you to learn another tool and structure your script in a particular way. Like in most programming languages, data structures in Elvish can be arbitrarily nested. This allows you to express more complex workflows in a natural way: ```elvish update-servers-in-parallel-v3.elv var hosts = [[&name=a &cmd='apt update' &dotfiles=[.tmux.conf .gitconfig]] [&name=b &cmd='pacman -Syu' &dotfiles=[.vimrc]]] peach {|h| ssh root@$h[name] $h[cmd] > ssh-$h[name].log scp ~/(all $h[dotfiles]) root@$h[name]: } $hosts ``` The expression `(all $h[dotfiles])` evaluates to all the elements of `$h[dotfiles]` (see documentation for [`all`](../ref/builtin.html#all)), each of which is then [combined](../ref/language.html#compounding) with `~/`, which evaluates to the home directory. Traditional shells tend to have limited support for complex data structures, so it can get quite tricky to express the same workflow, especially when coupled with parallel execution. What's more, you can easily move the definition of `hosts` into a JSON file - this can be useful if you'd like to share the script with others without requiring everyone to customize the script: ```json hosts.json [ {"name": "a", "cmd": "apt update", "dotfiles": [".tmux.conf", ".gitconfig"]}, {"name": "b", "cmd": "pacman -Syu", "dotfiles": [".vimrc"]} ] ``` ```elvish update-servers-in-parallel-v4.elv var hosts = (from-json < hosts.json) peach {|h| ssh root@$h[name] $h[cmd] > ssh-$h[name].log scp ~/(all $h[dotfiles]) root@$h[name]: } $hosts ``` Elvish brings the power of data structures and functional programming to your shell scripting scenarios. # Catching errors early The following interaction in the terminal showcases how Elvish is able to catch errors early: ```elvish-transcript Terminal: elvish ~> var project = ~/project ~> rm -rf $projetc/bin compilation error: variable $projetc not found ``` The example on the homepage is slightly simplified for brevity. In fact, Elvish will highlight the exact place of the error, before you even press Enter to execute the code: ```ttyshot Terminal: elvish learn/scripting-case-studies/misspelt-variable ``` In this case, Elvish identifies that the variable name is misspelt and won't execute the code. Compare this to an interaction in a more traditional shell: ```sh-transcript Terminal: sh $ project=~/project $ rm -rf $projetc/bin [ ...] ``` Traditional shells by default don't treat the misspelt variable as an error and evaluates it to an empty string instead. As the result, this will start executing `rm -rf /bin`, possibly with catastrophic consequences. Elvish's early error checking extends beyond terminal interactions, for example, suppose you have the following script: ```elvish script-with-error.elv var project = ~/project # A function... fn cleanup-bin { rm $projetc/bin } # More code... ``` If you try to run this script with `elvish script-with-error.elv`, Elvish will find the misspelt variable name within the function `cleanup-bin`, and refuses to execute any code from the script. Elvish's early error checking can help you prevent a lot of bugs from simple typos. There are more places Elvish checks for errors, and more checks are being added. # Command failures The following terminal interaction shows Elvish's behavior when a command fails: ```elvish-transcript Terminal: elvish ~> gm convert a.jpg a.png; rm a.jpg gm convert: Failed to convert a.jpg Exception: gm exited with 1 [tty 1]:1:1-22: gm convert a.jpg a.png; rm a.jpg # "rm a.jpg" is NOT executed ``` Like traditional shells, you can connect multiple commands together with either `;` (as in this example) or newlines, and Elvish will run them one after another. However, unlike traditional shells, if any command fails with a non-zero exit code, Elvish defaults to aborting execution. As the output indicates, this is part of a general mechanism of exceptions, which can be [caught](../ref/language.html#try) and [inspected](../ref/language.html#exception). (If you're familiar with the `set -e` mechanism in traditional shells, Elvish's behavior is similar, but without its [many](https://david.rothlis.net/shell-set-e/) [flaws](http://mywiki.wooledge.org/BashFAQ/105).) This early abortion behavior is a much safer default for scripting. In particular, it's almost certainly what you want for CI/CD scripts. For example, consider the following script: ```elvish ./run-tests ./run-linters ``` If `./run-tests` fails, Elvish will abort the entire script, but a traditional shell will happily proceed and only fail if `./run-linters` fails. elvish-0.21.0/website/learn/scripting-case-studies/000077500000000000000000000000001465720375400222355ustar00rootroot00000000000000elvish-0.21.0/website/learn/scripting-case-studies/misspelt-variable-ttyshot.elvts000066400000000000000000000001231465720375400304470ustar00rootroot00000000000000//cols 69 ~> var project = ~/project ~> #send-keys rm Space -rf Space $projetc/bin elvish-0.21.0/website/learn/scripting-case-studies/misspelt-variable-ttyshot.html000066400000000000000000000005441465720375400302650ustar00rootroot00000000000000~> var project = ~/project ~> rm -rf $projetc/bin elf@host compilation error: [interactive]:1:8-15: variable $projetc not found elvish-0.21.0/website/learn/tour.css000066400000000000000000000000461465720375400173470ustar00rootroot00000000000000td[colspan] { text-align: center; } elvish-0.21.0/website/learn/tour.md000066400000000000000000000720711465720375400171660ustar00rootroot00000000000000 # 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 familiar with traditional shells like Bash, the sections [basic shell language](#basic-shell-language) and [shell scripting commands](#shell-scripting-commands) can help you "translate" your knowledge into Elvish. 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 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
    Variables echo $foo
    var foo = bar foo=bar
    set foo = bar foo=bar
    { tmp foo = bar; some-command } foo=bar some-command
    Environment variables echo $E:HOME echo $HOME
    set E:foo = bar export foo=bar
    { tmp E:foo = bar; some-command } export foo; foo=bar some-command
    env foo=bar some-command
    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 &
    Command sequence a; b a && b
    ## 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, like `\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 extend 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: - They don't appear on their own, but as a suffix to `?`; - A character set is written like `[set:ch]`, instead of just `[ch]`. For example, to match files ending in either `.c` or `.h`, use: ```elvish-transcript ~> echo *.?[set:ch] foo.c foo.h ``` The suffix syntax means that they 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. ## Variables Like traditional shells, using the value of a variable requires the `$` prefix. ```elvish-transcript ~> var foo = bar ~> echo $foo bar ``` ### Field splitting Elvish does not perform `$IFS` splitting on variables, so `$foo` always evaluates to one value, even if it contains whitespaces and newlines: ```elvish-transcript ~> var foo = 'a b c d' ~> touch $foo # Creates one file ``` You never need to write `"$foo"` in Elvish. In fact, [double-quoted strings](#double-quoted-strings) do not support interpolation in Elvish, so `echo "$foo"` will just print out `$foo`). If you do need to split fields, you can either do this explicitly with [`str:fields`](../ref/str.html#str:fields), or store a list of strings and ["explode"](../ref/language.html#variable-use) it with `$@`: ```elvish-transcript ~> var args-as-string = 'a b c d' ~> use str ~> touch (str:fields $args-as-string) # creates four files ~> var args-as-list = [a b c d] ~> touch $@args-as-list # also creates four files ``` ### Declaring and setting variables Also 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. After declaring a variable, change its value with `set`: ```elvish-transcript ~> var foo = bar ~> echo $foo bar ~> set foo = quux ~> echo $foo quux ``` The spaces around `=` in both `var` and `set` are mandatory. Within a [lambda](#lambdas), you can use `tmp` to set the value for the duration of the lambda: ```elvish-transcript ~> var foo = bar ~> { tmp foo = new; echo $foo } new ~> echo $foo bar ``` Read the language reference on [variables](../ref/language.html#variable), [variable use](../ref/language.html#variable-use), [the `var` command](../ref/language.html#var), [the `set` command](../ref/language.html#set) and [the `tmp` command](../ref/language.html#tmp) to learn more. ## Environment variables Unlike traditional shells, environment variables in Elvish live in a separate `E:` namespace: ```elvish-transcript ~> echo $E:HOME /home/elf ~> set E:PATH = /bin:/sbin ``` There is no concept of "exporting" in Elvish: variables in the `E:` namespace are always "exported", and variables outside the namespace never are. Accessing unset environment variables results in an empty string: ```elvish-transcript ~> echo $E:nonexistent ``` Elvish also provides a series of builtin commands (`set-env`, `unset-env`, `has-env` and `get-env`) that allows you to distinguish unset environment variables and those set to an empty string. To set an environment variable temporarily, you can use the `tmp` command like you would with a non-environment variable, but it is more concise to use the external command `env`. ```elvish-transcript ~> { tmp E:foo = bar; bash -c 'echo $foo' } bar ~> env foo=bar bash -c 'echo $foo' bar ``` Read the language reference on the [`E:` namespace](../ref/language.html#special-namespaces), the [`set-env`](../ref/builtin.html#set-env), [`unset-env`](../ref/builtin.html#unset-env), [`has-env`](../ref/builtin.html#has-env) and [`get-env`](../ref/builtin.html#get-env) builtin commands 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. ## Command sequence Join commands with a `;` or newline to run them sequentially (insert a newline with Alt-Enter): ```elvish-transcript ~> echo a; echo b a b ~> echo a echo b a b ``` In Elvish, when a command fails (e.g. when an external command exits with a non-zero status), execution gets terminated. ```elvish-transcript ~> echo before; false; echo after before Exception: false exited with 1 [tty 2], line 1: echo before; false; echo after ``` In this aspect, Elvish's behavior is similar to joining all commands with `&&` or setting `set -e` in traditional shells: # 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 number type. There is no dedicated syntax for it; instead, it can constructed using the `num` builtin: ```elvish-transcript ~> num 1 ▶ (num 1) ~> num 1e2 ▶ (num 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 ▶ (num 3) ~> use math ~> math:pow (num 10) 3 ▶ (num 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 ``` 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 } catch 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 [environment variables](#environment-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 Read [the API of the interactive editor](../ref/edit.html) to learn more about UI customization options. ## Tab completion Press Tab to start completion. For example, after typing `vim` and Space, press Tab to complete filenames: ```ttyshot learn/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 learn/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 learn/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 learn/tour/history-walk-prefix ``` ### History listing Press Ctrl-R to list the full command history: ```ttyshot learn/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 learn/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 learn/tour/location ``` Type to filter: ```ttyshot learn/tour/location-filter ``` ## Navigation mode Press Ctrl-N to start the builtin filesystem navigator. ```ttyshot learn/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 interactive startup script is [`rc.elv`](../ref/command.html#rc-file). Non-interactive Elvish sessions do not have a startup script. ### POSIX aliases Elvish doesn't support POSIX 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. ### Prompt customization The left and right prompts can be customized by assigning functions to [`edit:prompt`](../ref/edit.html#$edit:prompt) and [`edit:rprompt`](../ref/edit.html#$edit:rprompt). The following example defines prompts similar to the default, but uses fancy Unicode. ```ttyshot learn/tour/unicode-prompts ``` The [`tilde-abbr`](../ref/builtin.html#tilde-abbr) command abbreviates home directory to a tilde. The [`constantly`](../ref/builtin.html#constantly) command returns a function that always writes the same value(s) to the value output. The [`styled`](../ref/builtin.html#styled) command writes styled output. ### Changing PATH Another common task in the interactive startup script is to set the search path. You can do set the environment variable directly (all environment variables have a `E:` prefix): ```elvish set E:PATH = /opts/bin:/bin:/usr/bin ``` But it is usually nicer to set the [`$paths`](../ref/builtin.html#$paths) instead: ```elvish set paths = [/opts/bin /bin /usr/bin] ``` # Shell scripting commands Elvish has its own set of [builtin commands](../ref/builtin.html). This section helps you find commands that correspond to commands in traditional shells. ## command To force Elvish to treat a command as an external command, prefix it with [`e:`](../ref/language.html#special-namespaces). ## export In Elvish, environment variables live in the [`E:`](../ref/language.html#special-namespaces) namespace. There is no concept of exporting a variable to the environment; environment variables are always "exported" to child processes, and non-environment variables never are. ## source To build reusable libraries, use Elvish's [module mechanism](../ref/language.html#modules). To execute a dynamic piece of code for side effect, use [`eval`](../ref/builtin.html#eval). If the code lives in a file, write `eval (slurp < /path/to/file)`. Due to Elvish's scoping rules, files executed using either of the mechanism above can't create new variables in the current namespace. For example, `eval 'var foo = bar'; echo $foo` won't work. However, the REPL's namespace *can* be manipulated with [`edit:add-var`](../ref/edit.html#edit:add-var). ## test To test files, use commands in the [path](../ref/path.html) module. To compare numbers, use number comparison commands like [`<`](../ref/builtin.html#num-lt). To compare strings, use string comparison commands like [` cd elvish ~> #send-keys echo Space Tab .md elvish-0.21.0/website/learn/tour/completion-filter-ttyshot.html000066400000000000000000000005561465720375400247010ustar00rootroot00000000000000~> cd elvish ~/elvish> echo 1.0-release.md elf@host COMPLETING argument .md 1.0-release.md PACKAGING.md SECURITY.md CONTRIBUTING.md README.md elvish-0.21.0/website/learn/tour/completion-ttyshot.elvts000066400000000000000000000000521465720375400235760ustar00rootroot00000000000000~> cd elvish ~> #send-keys echo Space Tab elvish-0.21.0/website/learn/tour/completion-ttyshot.html000066400000000000000000000013051465720375400234070ustar00rootroot00000000000000~> cd elvish ~/elvish> echo 1.0-release.md elf@host COMPLETING argument 1.0-release.md README.md syntaxes/ CONTRIBUTING.md SECURITY.md tools/ Dockerfile cmd/ vscode/ LICENSE go.mod website/ Makefile go.sum PACKAGING.md pkg/ elvish-0.21.0/website/learn/tour/history-list-ttyshot.elvts000066400000000000000000000000221465720375400240740ustar00rootroot00000000000000~> #send-keys C-R elvish-0.21.0/website/learn/tour/history-list-ttyshot.html000066400000000000000000000026041465720375400237130ustar00rootroot00000000000000~> elf@host HISTORY (dedup on) Ctrl-D dedup 3 echo "hello\nbye" > /tmp/x 4 from-lines < /tmp/x 5 cd /tmp 6 cd ~/elvish 7 git branch 8 git checkout . 9 git commit 19 git status 20 cd /usr/local/bin 21 echo $pwd 22 * (+ 3 4) (- 100 94) 31 make 32 math:min 3 1 30 elvish-0.21.0/website/learn/tour/history-walk-prefix-ttyshot.elvts000066400000000000000000000000341465720375400253550ustar00rootroot00000000000000~> #send-keys echo Up Up Up elvish-0.21.0/website/learn/tour/history-walk-prefix-ttyshot.html000066400000000000000000000005441465720375400251720ustar00rootroot00000000000000~> echo (styled warning: red) bumpy road elf@host HISTORY #2 elvish-0.21.0/website/learn/tour/history-walk-ttyshot.elvts000066400000000000000000000000211465720375400240560ustar00rootroot00000000000000~> #send-keys Up elvish-0.21.0/website/learn/tour/history-walk-ttyshot.html000066400000000000000000000005611465720375400236760ustar00rootroot00000000000000~> math:min 3 1 30 elf@host Ctrl-A autofix: use math Tab Enter autofix first HISTORY #32 elvish-0.21.0/website/learn/tour/lastcmd-ttyshot.elvts000066400000000000000000000000541465720375400230560ustar00rootroot00000000000000~> echo abc def ~> #send-keys vim Space M-, elvish-0.21.0/website/learn/tour/lastcmd-ttyshot.html000066400000000000000000000005261465720375400226710ustar00rootroot00000000000000~> echo abc def abc def ~> vim elf@host LASTCMD echo abc def 0 echo 1 abc 2 def elvish-0.21.0/website/learn/tour/location-filter-ttyshot.elvts000066400000000000000000000000301465720375400245140ustar00rootroot00000000000000~> #send-keys C-L local elvish-0.21.0/website/learn/tour/location-filter-ttyshot.html000066400000000000000000000004501465720375400243310ustar00rootroot00000000000000~> elf@host LOCATION local 10 ~/.local/share/elvish 9 /usr/local 9 /usr/local/share 9 /usr/local/bin elvish-0.21.0/website/learn/tour/location-ttyshot.elvts000066400000000000000000000000221465720375400232320ustar00rootroot00000000000000~> #send-keys C-L elvish-0.21.0/website/learn/tour/location-ttyshot.html000066400000000000000000000024571465720375400230570ustar00rootroot00000000000000~> elf@host LOCATION 10 ~/elvish 10 ~/.local/share/elvish 10 ~/elvish/website 10 ~/.config/elvish 9 ~/elvish/pkg/edit 9 ~/elvish/pkg/eval 9 /opt 9 /usr/local 9 /usr/local/share 9 /usr/local/bin 9 /usr 9 /tmp 8 ~/zsh elvish-0.21.0/website/learn/tour/navigation-ttyshot.elvts000066400000000000000000000000551465720375400235670ustar00rootroot00000000000000~> cd elvish; echo '[CUT]' ~> #send-keys C-N elvish-0.21.0/website/learn/tour/navigation-ttyshot.html000066400000000000000000000024471465720375400234050ustar00rootroot00000000000000~/elvish> elf@host NAVIGATING Ctrl-H hidden Ctrl-F filter bash 1.0-release.m 1.0 has not been released y elvis CONTRIBUTING. zsh Dockerfile LICENSE Makefile PACKAGING.md README.md SECURITY.md cmd go.mod go.sum pkg syntaxes elvish-0.21.0/website/learn/tour/unicode-prompts-ttyshot.elvts000066400000000000000000000003311465720375400245550ustar00rootroot00000000000000~> set edit:rprompt = (constantly ^ (styled (whoami)✸(hostname) inverse)) ~> set edit:prompt = { tilde-abbr $pwd styled '❱ ' bright-red } ~> #send-keys # Space Fancy Space unicode Space prompts! elvish-0.21.0/website/learn/tour/unicode-prompts-ttyshot.html000066400000000000000000000016671465720375400244010ustar00rootroot00000000000000~> set edit:rprompt = (constantly ^ (styled (whoami)(hostname) inverse)) ~> set edit:prompt = { tilde-abbr $pwd styled '❱ ' bright-red } ~# Fancy unicode prompts! elf✸host.example.com elvish-0.21.0/website/learn/unique-semantics.md000066400000000000000000000342771465720375400214750ustar00rootroot00000000000000 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 ~> var 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'] } ~> var 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'] } ~> var 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 var 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 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 ~> var m = [&foo=bar &lorem=ipsum] ~> var m2 = $m ~> set 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 set m2[foo] = quux # is just syntax sugar for: set 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.21.0/website/learn/value-types.md000066400000000000000000000274201465720375400204510ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - [Your first Elvish commands](first-commands.html) - [Arguments and outputs](arguments-and-outputs.html) - [Variables and loops](variables-and-loops.html) - [Pipelines and IO](pipelines-and-io.html) - **Value types** - [Organizing and reusing code](organizing-and-reusing-code.html) # Maps We have learned how you can use `curl` to request a URL and `from-json` to convert JSON-encoded bytes to Elvish values. Combining these two features allows us to import data from online JSON APIs: for example, let's use the API from to query our IP address and country: ```elvish-transcript Terminal - elvish ~> curl -s https://api.myip.com {"ip":"10.0.0.31","country":"Elvendom","cc":"EL"} ~> curl -s https://api.myip.com | from-json ▶ [&cc=EL &country=Elvendom &ip=10.0.0.31] ``` The result is surrounded by `[` and `]`, just like lists; but rather than a list, it's actually a new type of data structure called **maps**. Lists consist of elements. Maps, on the other hand, consists of *pairs* of **keys** and **values**. In Elvish, maps are written like `&key=value`, and putting writing inside `[` and `]` make the overall data structure a map. (The [general concept of maps](https://en.wikipedia.org/wiki/Associative_array) is present in many other languages. In this case, our map is actually converted from a JSON "object", which you can also think of as a map.) The key-value structure is useful because if there's a key you know, you can find the corresponding value by **indexing** the map: ```elvish-transcript Terminal - elvish ~> curl -s https://api.myip.com | put (from-json)[country] ▶ Elvendom ``` The `[country]` used for indexing uses the same `[` and `]` for writing lists and maps, but has a different meaning in this context. You can even use them together: ```elvish-transcript Terminal - elvish ~> put [&country=Elvendom][country] ▶ Elvendom ``` Here, the first pair of `[]` delimits a map, and the second pair delimits the index. To examine the map without having to make the same request every time, we can save it in a variable: ```elvish-transcript Terminal - elvish ~> curl -s https://api.myip.com | var info = (from-json) ~> put $info[country] ▶ Elvendom ~> put $info[cc] ▶ EL ``` # List indexing We can also use the indexing syntax to retrieve an element of a list by its position: ```elvish-transcript Terminal - elvish ~> var triumvirate = [Julius Crassus Pompey] ~> echo $triumvirate[0]' is the most powerful' Julius is the most powerful ``` The index [starts at zero](https://en.wikipedia.org/wiki/Zero-based_numbering), like in many other programming languages. Instead of just one element, lists also allow you to retrieve a **slice** of elements. There are two variants: *i*..*j* starts from *i* and doesn't include *j*, while *i*..=*j* includes *j*: ```elvish-transcript Terminal - elvish ~> put $triumvirate[0..2] ▶ [Julius Crassus] ~> put $triumvirate[0..=2] ▶ [Julius Crassus Pompey] ``` As we can see, the result is another list. This is true even if the result is one or even zero element: ```elvish-transcript Terminal - elvish ~> put $triumvirate[0..1] ▶ [Julius] ~> put $triumvirate[0..0] ▶ [] ``` # Nesting data structures Lists and maps in Elvish can be arbitrarily *nested*. You can have a list of lists, for example to represent tabular data: ```elvish-transcript Terminal - elvish ~> var table = [[6 10 2] [-2 0 10]] ~> put $table[0][1] ▶ 10 ``` You can have a map where each value is another map, for example to represent information about different entities (in this case, great ancient people) ```elvish-transcript Terminal - elvish ~> var people = [&Julius=[&title=Dictator &country=Rome] &Alexander=[&title=King &country=Macedon]] ~> put $people[Julius][title] ▶ Dictator ``` You can even use lists and maps as map keys: ```elvish-transcript Terminal - elvish ~> var map-of-complex-keys = [&[&foo=bar]=map &[foo bar]=list] ~> put $map-of-complex-keys[[&foo=bar]] ▶ map ~> put $map-of-complex-keys[[foo bar]] ▶ list ``` (This example can be a bit of a brain teaser because all three meanings of `[` and `]` are used. Using maps and lists as map keys is not super common, but it's convenient when you do need it.) The possibilities are limitless -- as long as the data fits in your computer's RAM and the nesting relationship in your brain. And it's not just for theoretical interest; for a real-world example of a list of maps with list values, see [the `update-servers-in-parallel.elv` case study](https://xiaq.me/draft.elv.sh/learn/scripting-case-studies.html#update-servers-in-parallel.elv). (We'll learn more about some features in that example in [Organizing and reusing code](organizing-and-reusing-code.html)). # Strings String is a value type that we have actually been using the whole time. Any text not using any special punctuation or whitespaces are strings, as are quoted strings. Unquoted strings are also known as **barewords**. # Numbers In fact, even numbers like `1` in Elvish are strings. Let's examine this example: ```elvish-transcript Terminal - elvish ~> + 1 2 ▶ (num 3) ~> + '1' '2' ▶ (num 3) ``` In this example, both `1` and `2` are strings, so `+ 1 2` and `+ '1' '2'` are equivalent. Numerical commands like `+` accept strings and know how to treat them as numbers internally. This is OK for the most part, but there are situations where using strings as numbers doesn't do what you need: ```elvish-transcript Terminal - elvish ~> put 1 | to-json "1" ~> put 1 2 12 | order ▶ 1 ▶ 12 ▶ 2 ``` (The [`order`](../ref/builtin.html#order) command reads value inputs, and outputs them sorted.) Elvish also supports a number type, and number values can be constructed using the [`num`](../ref/builtin.html#num) command. You can use them as arguments to numerical commands too, and they behave as numbers when converting to JSON or sorting: ```elvish-transcript Terminal - elvish ~> num 3 ▶ (num 3) ~> + (num 1) (num 2) ▶ (num 3) ~> num 1 | to-json 1 ~> put (num 1) (num 2) (num 12) | order ▶ (num 1) ▶ (num 2) ▶ (num 12) ``` Since commands like `+` accept both `1` and `(num 1)`, we'll call both "numbers". To distinguish the latter from the former, we usually call them **typed numbers**. As you have probably inferred from how the outputs were shown, even though numerical commands accept both strings and typed numbers as arguments, they always output typed numbers. This makes the result easier to use in contexts like converting to JSON or sorting. We have only worked with integers so far, but Elvish also supports rational numbers and floating-point numbers: ```elvish-transcript Terminal - elvish ~> + 1/2 1/3 ▶ (num 5/6) ~> + 0.2 0.3 ▶ (num 0.5) ``` # Booleans The [Boolean type](https://en.wikipedia.org/wiki/Boolean_data_type) has two values, *true* and *false*, written in Elvish as two variables `$true` and `$false`. Unsurprisingly, Elvish commands that tell you if something is true or false output Boolean values: ```elvish-transcript Terminal - elvish ~> use str ~> str:has-suffix a.png .png ▶ $true ~> str:has-suffix a.png .jpg ▶ $false ``` Elvish also supports [Boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra) operations: ```elvish-transcript Terminal - elvish ~> and $true $false ▶ $false ~> or $true $false ▶ $true ~> not $true ▶ $false ``` ## Conditionals Boolean values can be used to decide whether to do something or not, with the help of the [`if`](../ref/language.html#if) command: ```elvish-transcript Terminal - elvish ~> if $true { echo "Yes it's true" } Yes it's true ~> if $false { echo "This shouldn't happen" } ``` The `if` command is a [**conditional**](https://en.wikipedia.org/wiki/Conditional_(computer_programming)) command, one of the most basic [**control flows**](https://en.wikipedia.org/wiki/Control_flow). For loops, which we have seen earlier, are another type of control flow. Extending our previous example of converting JPG files to AVIF, let's add an additional condition: we should only perform the conversion when the AVIF file doesn't exist yet: ```elvish-transcript Terminal - elvish ~> use os ~> use str ~> for jpg [*.jpg] { var avif = (str:trim-suffix $jpg .jpg).avif if (not (os:exists $avif)) { # new condition gm convert $jpg $avif } } ``` # Value pipeline redux Using what we've learned about values in Elvish, let's build a more interesting value pipeline: ```elvish-transcript Terminal - elvish ~> curl -s https://xkcd.com/info.0.json | var latest = (from-json)[num] # ① ~> range $latest (- $latest 5) | # ② each {|n| curl -s https://xkcd.com/$n/info.0.json } | # ③ from-json | # ④ each {|info| echo $info[num]': '$info[title] } # ⑤ 2905: Supergroup 2904: Physics vs. Magic 2903: Earth/Venus Venn Diagram 2902: Ice Core 2901: Geographic Qualifiers ``` The code above prints the latest 5 webcomics from , using its [JSON API](https://xkcd.com/json.html). Let's examine it step by step: 1. We first request , which fetches the information for the latest comic. We convert that to an Elvish map, and save the value of `num` in `$latest`. 2. The [`range`](../ref/builtin.html#range) command outputs a range of numbers. The range can be increasing or decreasing, but in both cases, it starts at the first argument, and stops *before* it reaches the second argument. In the example output, `$latest` happens to be 2905, so `(- $latest 5)` is 2900. You can see the `range` command in action by running it separately: ```elvish-transcript Terminal - elvish ~> range 2905 2900 ▶ (num 2905) ▶ (num 2904) ▶ (num 2903) ▶ (num 2902) ▶ (num 2901) ``` 3. The [`each`](../ref/builtin.html#each) command runs the code inside `{` and `}`, assigning `$n` to each input (we will cover the syntax soon in [Organizing and reusing code](organizing-and-reusing-code.html)). It's similar to the `for` command we have seen before, except that it uses input values rather than list elements. The overall effect is the same as: ```elvish-transcript Terminal - elvish ~> curl -s https://xkcd.com/2095/info.0.json curl -s https://xkcd.com/2094/info.0.json curl -s https://xkcd.com/2093/info.0.json curl -s https://xkcd.com/2092/info.0.json curl -s https://xkcd.com/2091/info.0.json ... ``` (To input multiple lines of commands at the prompt, press Alt-Enter instead of Enter.) 4. The `from-json` command converts the stream of JSON outputs generated by all the `curl` commands into a stream of Elvish values, in this case maps. 5. This second `each` command retrieves the values corresponding to the `num` and `title` keys, and print them. ## Limitations of value pipelines Value pipelines allow you to manipulate data in a natural way similar to traditional byte pipelines, but it does have an important shortcoming: it is not available to external commands. We have already seen that external commands can't produce value outputs; they also can't consume value inputs. As a result, if you write an Elvish command that produces value outputs, you have to convert it to bytes before an external command can make use of it, such as with the `to-json` command. # Conclusion Elvish has a rich system of value types. These types allow you to model real-world problems however you want, and consume and manipulate data sourced from elsewhere. Elvish's value pipeline mechanism allows you to express these operations in a fluid way. Let's now move on to the final part of this series, [Organizing and reusing code](organizing-and-reusing-code.html). elvish-0.21.0/website/learn/variables-and-loops.md000066400000000000000000000344621465720375400220410ustar00rootroot00000000000000 This article is part of the *Beginner's Guide to Elvish* series: - [Your first Elvish commands](first-commands.html) - [Arguments and outputs](arguments-and-outputs.html) - **Variables and loops** - [Pipelines and IO](pipelines-and-io.html) - [Value types](value-types.html) - [Organizing and reusing code](organizing-and-reusing-code.html) # Using variables In [Your first Elvish commands](first-commands.html), we saw [an example](first-commands.html#a-concrete-example) of how to use a series of commands to download Elvish. Let's focus on the initial two commands, which download the archive and show the SHA256 checksum respectively: ```elvish-transcript Terminal - elvish ~> curl -s -o elvish-HEAD.tar.gz https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz ~> curl -s https://dl.elv.sh/linux-amd64/elvish-HEAD.tar.gz.sha256sum 93b206f7a5b7f807f6b2b2b99dd4074ed678620541f6e9742148fede0a5fefdb elvish-HEAD.tar.gz ``` This example comes with a catch -- it only works as long as the `linux-amd64` part actually matches your platform, namely Linux on a [x86-64 CPU](https://en.wikipedia.org/wiki/X86-64#AMD64). To fix that, instead of *hardcoding* this string, we need a way to construct it *dynamically* to actually match your platform. Turns out that Elvish already has all the information we need, stored inside two **variables**: ```elvish-transcript Terminal - elvish ~> use platform ~> echo $platform:os darwin ~> echo $platform:arch arm64 ``` (We'll learn what `use platform` and the colons are about in [Organizing and reusing code](organizing-and-reusing-code.html)). The `$` character starts a variable, and tells Elvish to **evaluate** it to the value stored inside it. In this case, the `$platform:os` variable stores a string identifying the OS ([`darwin`](https://en.wikipedia.org/wiki/Darwin_(operating_system)) in the example output), and the `$platform:arch` variable stores a string identifying the CPU architecture ([`arm64`](https://en.wikipedia.org/wiki/AArch64) in the example output). Your output may differ, but at least in the example output, it turns out our platform doesn't match `linux-amd64` after all. Let's now fix our command by making use of these variables: ```elvish-transcript Terminal - elvish ~> curl -s -o elvish-HEAD.tar.gz https://dl.elv.sh/$platform:os'-'$platform:arch/elvish-HEAD.tar.gz ~> curl -s https://dl.elv.sh/$platform:os'-'$platform:arch/elvish-HEAD.tar.gz.sha256sum f1b2e7c149f5104c191bc7c9cd922b87ac73d810ba71c186636d1807e2a5ce95 elvish-HEAD.tar.gz ``` And now our commands work regardless of which platform we are on! In more fancy terms, our commands are now *portable* across platforms. Let's recap what is going on: 1. Elvish sees `$platform:os` and `$platform:arch` and evaluates them to their respective values -- in our environment, `darwin` and `arm64` respectively. 2. Elvish concatenates them to the neighboring strings to form the overall argument. The argument for the first `curl` command is ; similarly for the second `curl` command, with an extra `.sha256sum` suffix. 3. The `curl` command then runs with the arguments we have constructed. (There is still a catch: this example still doesn't work for Windows, because the archive files for Windows end in `.zip` instead of `.tar.gz`. Once we have learned conditionals in [Value types](value-types.html), you can come back here to make this code fully portable.) ## Quoting and syntax highlighting Notice how we quoted `-` between `$platform:os` and `$platform:arch`. This is because variable names in Elvish can include `-`, so if we omit it, Elvish will try to find the variable `$platform:os-`: ```elvish-transcript Terminal - elvish ~> curl -s -o elvish-HEAD.tar.gz https://dl.elv.sh/$platform:os-$platform:arch/elvish-HEAD.tar.gz Exception: variable $platform:os- not found ``` This introduces us to another reason for quoting strings: when concatenating literal strings with variables, quoting the literal part can stop Elvish from treating it as part of the variable name. Elvish also gives you hints using by **highlighting** different parts of the code. Let's zoom in on the part around our variables: ```elvish-transcript Terminal - elvish ~> echo $platform:os'-'$platform:arch darwin-arm64 ~> echo $platform:os-$platform:arch Exception: variable $platform:os- not found ``` In the first correct command, the quoted `'-'` has a distinct color, clearly standing out from the variables around it. In the second incorrect command, the unquoted `-` is colored the same as variables, meaning that Elvish will treat it as part of the variable name. # Defining new variables Our commands for downloading Elvish and showing the checksum still has some room for improvement. Notice how similar the two commands are, in particular the last argument: ```elvish-transcript Terminal - elvish ~> curl -s -o elvish-HEAD.tar.gz https://dl.elv.sh/$platform:os'-'$platform:arch/elvish-HEAD.tar.gz ~> curl -s https://dl.elv.sh/$platform:os'-'$platform:arch/elvish-HEAD.tar.gz.sha256sum f1b2e7c149f5104c191bc7c9cd922b87ac73d810ba71c186636d1807e2a5ce95 elvish-HEAD.tar.gz ``` To fix that, we can store the common part in a new variable: ```elvish-transcript Terminal - elvish ~> var archive-url = https://dl.elv.sh/$platform:os'-'$platform:arch/elvish-HEAD.tar.gz ~> curl -s -o elvish-HEAD.tar.gz $archive-url ~> curl -s $archive-url.sha256sum f1b2e7c149f5104c191bc7c9cd922b87ac73d810ba71c186636d1807e2a5ce95 elvish-HEAD.tar.gz ``` The [`var` command](../ref/language.html#var) defines a new variable called `archive-url` and gives it an initial value. After that, we can use it like `$archive-url`. Notice how we don't use the `$` prefix when defining a variable. This is because `$` instructs Elvish to evaluate a variable, and we are not doing that when defining it. However, we may still say that we "define `$archive-url`" as a shorthand of "define the `archive-url` variable". # For loops and lists The ability to use and define variables gives us the flexibility in how we do *one* thing, but often we find ourselves repeating similar but not entirely identical tasks. For example, let's say we have a few `.jpg` files that we would like to convert into the more efficient [AVIF](https://en.wikipedia.org/wiki/AVIF) format. (If you'd like to follow this example but don't have spare `.jpg` files lying around, download some from [Wikimedia Commons](https://commons.wikimedia.org/w/index.php?search=seashell&title=Special:MediaSearch&go=Go&type=image&filemime=jpeg).) With the `gm` command provided by [GraphicsMagick](http://www.graphicsmagick.org), we can convert them one by one: ```elvish-transcript Terminal - elvish ~> gm convert banana.jpg banana.avif ~> gm convert unicorn.jpg unicorn.avif ~> # and so on... ``` There is a better way to do it, though. Like many other programming languages, Elvish provides *loops* to perform repetitive work: ```elvish-transcript Terminal - elvish ~> use str # ① ~> for jpg [banana.jpg unicorn.jpg] { # ② var avif = (str:trim-suffix $jpg .jpg).avif # ③ gm convert $jpg $avif # ④ } ``` This is a more complex example, so let's go through it line by line: 1. `use str` imports the [`str` module](../ref/str.html). We'll learn about modules in [Organizing and reusing code](organizing-and-reusing-code.html); for now, it suffices to know that this is needed to be able to use `str:trim-suffix` below. 2. The [`for`](../ref/language.html#for) command introduces a **for loop**. Let's first focus on `[banana.jpg unicorn.jpg]`: the `[` and `]` delimits a **list**, a type of value that consists of multiple **elements**. Here, the elements are `banana.jpg` and `unicorn.jpg`, separated by spaces -- just like how the arguments to a command are separated by spaces. The for loop works as follows: for each element of the list, it defines the `jpg` variable to be equal to that element, and runs the code inside `{` and `}` (the **body** of the for loop). Now for the body itself... 3. Since the name of the input JPG file is no longer hardcoded, we can no longer hardcode the name of the output AVIF file either. Instead, we use some string manipulation to derive the output name from the input name - the [`str:trim-suffix`](../ref/str.html#str:trim-suffix) commands removes a fixed suffix from a string. You can see it in action like this: ```elvish-transcript Terminal - elvish ~> str:trim-suffix banana.jpg .jpg ▶ banana ``` We then concatenate the result with `.avif` to form the output filename, in this case `banana.avif`, and store it in the `$avif` variable. 4. Finally, we use the `gm` command to perform the conversion. As we can see, the for loop will run the body twice, once with `$foo` equal to `banana.jpg`, and once with `$foo` equal to `unicorn.jpg`, so this achieves the same effect as two "manual" invocations `gm` that we set out to improve. ## The strength of loops In this particular case, we haven't really achieved any improvement -- our new code is longer and more complex than the two separate `gm` invocations. In fact, when you only need to repeat a simple task twice or three times, just repeating it "manually" -- probably with the help of Elvish's command history -- is a totally valid approach. The real strength of for loops is when there are many elements, maybe even an unknown number of them. Let's say we'd like to convert *all* the `.jpg` files to `.avif` files. With the manual approach you'd have to write as many `gm` commands as there are files, but with a for loop, just a simple modification is needed: ```elvish-transcript Terminal - elvish ~> use str ~> for jpg [*.jpg] { # ① var avif = (str:trim-suffix $jpg .jpg).avif gm convert $jpg $avif } ``` Here, we have changed the element of the list to be `*.jpg` -- this doesn't represent a single file named `*.jpg`, but is a stand-in for all the filenames ending in `.jpg`. Here, our for loop is able to handle the conversion comfortably, whether it's just one file or thousands of files. ## Wildcards The `*.jpg` we have just seen is an example of **wildcard patterns**. Here, `*` is a **wildcard character** that can match any number of characters, so `*.jpg` matches `banana.jpg`, `unicorn.jpg`, or even `.jpg` if there happens to be such a file. The [wildcard expansion](../ref/language.html#wildcard-expansion) section of the language reference describes wildcards in more details, but `*` is perhaps what you will use most of the time. # Multiple values Something worth remarking with the behavior of `*.jpg` is that it evaluates to **multiple values**. This means that it becomes multiple elements in a list, which is what's happening here, but it also becomes multiple arguments when used in commands. We can see this most clearly with [the `put` command](../ref/builtin.html#put), which writes each of its argument as a value output: ```elvish-transcript Terminal - elvish ~> put *.jpg ▶ banana.jpg ▶ unicorn.jpg ``` ## Output capture redux Previously, we have captured the outputs of commands to use as arguments to other commands, like this: ```elvish-transcript Terminal - elvish ~> * (+ 2 10) 3 ▶ (num 36) ``` Here, `(+ 2 10)` outputs a single value, which then gets used as a single argument. Some commands in Elvish can output multiple values, and capturing their output gives us multiple values too. For example, the [`str:split`](../ref/str.html#str:split) command splits a string around a separator, outputting one value for each split results: ```elvish-transcript Terminal - elvish ~> str:split , friends,Romands,countrymen ▶ friends ▶ Romands ▶ countrymen ``` We can use these multiple values in the same way we used the multiple values generated `*.jpg`. For example, we can put them in a list and use that in a for loop: ```elvish-transcript Terminal - elvish ~> for who [(str:split , friends,Romans,countrymen)] { echo 'Hello, '$who'!' } Hello, friends! Hello, Romans! Hello, countrymen! ``` Both `+` and `str:split` output values, but what about commands that output bytes? When we capture their output, each *line* becomes a value. As an example, is a file listing all the files available on the site. We can use `curl` to request this file and capture the output: ```elvish-transcript Terminal - elvish ~> for url [(curl -s https://dl.elv.sh/INDEX)] { echo 'URL: '$url } URL: https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz URL: https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz.sha256sum ... ``` For the purpose of examining values, we don't have to put them in a list and use a for loop. Remember the `put` command, which turns each argument into a value in its output: ```elvish-transcript Terminal - elvish ~> put (curl -s https://dl.elv.sh/INDEX) ▶ https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz ▶ https://dl.elv.sh/darwin-amd64/elvish-HEAD.tar.gz.sha256sum ... ``` ## Lists vs multiple values A list in Elvish *stores* multiple values, but it's always one value itself. In some shells and other programming languages, lists can implicitly "become" multiple values -- that never happens in Elvish. We have seen how you can turn multiple values into a list simply by wrapping them inside a pair of `[` and `]`. Conversely, when you have a list and would like to get all its elements as separate values, you can use the [`all`](../ref/builtin.html#all) command, which does exactly that: ```elvish-transcript Terminal - elvish ~> all [foo bar] ▶ foo ▶ bar ``` If the list happens to be stored inside a variable `$list`, you can also use the shorthand `$@list`: ```elvish-transcript Terminal - elvish ~> var list = [foo bar] ~> put $@list ▶ foo ▶ bar ``` # Conclusion Variables, lists and loops are basic but important [abstraction](https://en.wikipedia.org/wiki/Abstraction_(computer_science)) mechanisms in programming, and shell scripting is no exception. In this part, we've learned how to use variables to go beyond simple hardcoded commands and adapt them to the context they operate in. We've also used loops, lists and wildcards to repeat operations without even knowing in advance how many times to repeat them for, and dived into how to make use of multiple values. We are now ready for the next part, [Pipelines and IO](pipelines-and-io.html). elvish-0.21.0/website/ref/000077500000000000000000000000001465720375400153175ustar00rootroot00000000000000elvish-0.21.0/website/ref/builtin.md000066400000000000000000000130741465720375400173140ustar00rootroot00000000000000 @module builtin # 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](language.html#scoping-rule), and Elvish's namespacing mechanism makes it impossible for other modules to redefine builtin symbols. It's almost always sufficient (and safe) to use builtin functions and variables with their unqualified names. Nonetheless, the builtin module is also available as a [pre-defined module](language.html#pre-defined-modules). It can be imported with `use builtin`, which makes all the builtin symbols available under the `builtin:` namespace. This can be useful in several cases: - To refer to a builtin function when it is shadowed locally. This is especially useful when the function that shadows the builtin one is a wrapper: ```elvish use builtin fn cd {|@args| echo running my cd function builtin:cd $@args } ``` Note that the shadowing of `cd` is only in effect in the local lexical scope. - To introspect 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 is 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 $inputs? ``` 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 a `sep` option and arbitrary arguments: ```elvish echo &sep=' ' $value... ``` (When you call functions, options are always optional.) ## Commands taking value inputs {#value-inputs} Most commands that take value inputs (e.g. `count`, `each`) can take the inputs in one of two ways: 1. From the pipeline: ```elvish-transcript ~> put lorem ipsum | count # count number of inputs 2 ~> put 10 100 | each {|x| + 1 $x } # apply function to each input ▶ (num 11) ▶ (num 101) ``` If the previous command outputs bytes, one line becomes one string input, as if there is an implicit [`from-lines`]() (this behavior is subject to change): ```elvish-transcript ~> print "a\nb\nc\n" | count # count number of lines ▶ 3 ~> use str ~> print "a\nb\nc\n" | each $str:to-upper~ # apply to each line ▶ A ▶ B ▶ C ``` 2. 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 supported: ```elvish-transcript ~> count lorem ▶ 5 ``` When documenting such commands, the optional argument is always written as `$inputs?`. **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 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 Wherever 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. ## Unstable features 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.) elvish-0.21.0/website/ref/command.md000066400000000000000000000142711465720375400172640ustar00rootroot00000000000000 # 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). (To use Elvish as your default shell, see [this page](../get/default-shell.html)). 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: 1. If the legacy `~/.elvish/rc.elv` exists, it is used (this will be ignored starting from 0.21.0). 2. If the `XDG_CONFIG_HOME` environment variable is defined and non-empty, `$XDG_CONFIG_HOME/elvish/rc.elv` is used. 3. Otherwise, `~/.config/elvish/rc.elv` (non-Windows OSes) or `%AppData%\elvish\rc.elv` (Windows) 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: 1. If the legacy `~/.elvish/db` exists, it is used (this will be ignored starting from 0.21.0). 2. If the `XDG_STATE_HOME` environment variable is defined and non-empty, `$XDG_STATE_HOME/elvish/db.bolt` is used. 3. Otherwise, `~/.local/state/elvish/db.bolt` (non-Windows OSes) or `%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). When running a script, Elvish does not evaluate the [RC file](#rc-file). # Module search directories When importing [modules](language.html#modules), Elvish searches the following directories: 1. If the `XDG_CONFIG_HOME` environment variable is defined and non-empty, `$XDG_CONFIG_HOME/elvish/lib` is searched. Otherwise, `~/.config/elvish/lib` (non-Window OSes) or `%RoamingAppData%\elvish\lib` (Windows) is searched. 2. If the `XDG_DATA_HOME` environment variable is defined and non-empty, `$XDG_DATA_HOME/elvish/lib` is searched. Otherwise, `~/.local/share/elvish/lib` (non-Windows OSes) or `%LocalAppData%\elvish\lib` (Windows) is searched. 3. If the `XDG_DATA_DIRS` environment variable is defined and non-empty, it is treated as a colon-delimited list of paths (semicolon-delimited on Windows), which are all searched. Otherwise, `/usr/local/share/elvish/lib` and `/usr/share/elvish/lib` are searched on non-Windows OSes. On Windows, no directories are searched. 4. If the legacy `~/.elvish/lib` directory exists, it is also searched (this will be ignored starting from 0.21.0). # Command-line flags - `-buildinfo`: Output information about the Elvish build and quit. See also `-version` and `-json`. - `-c`: Treat the first argument as code to execute, instead of name of file to execute. See [running a script](#running-a-script). - `-compileonly`: Parse and compile Elvish code without executing it. Useful for checking parse and compilation errors. Currently ignored when Elvish is run [interactively](#using-elvish-interactively) (so can't be used to check the [RC file](#rc-file), for example). - `-deprecation-level n`: Show warnings for features deprecated as of version 0.*n*. In release builds, the default value matches the release version, and this flag is mainly useful for hiding newly introduced deprecation warnings. For example, if you have upgraded from 0.41 to 0.42, you can use `-deprecation-level 41` to hide deprecation warnings introduced in 0.42, before you have time to fix those warnings. In HEAD builds, the default value matches the *previous* release version, and this flag is mainly useful for previewing upcoming deprecations. For example, if you are running a HEAD version between the 0.42.0 release and 0.43.0 release, you can use `-deprecation-level 43` to preview deprecations that will be introduced in 0.43.0. - `-help`: Show usage help and quit. - `-i`: A no-op flag, introduced for POSIX compatibility. In future, this may be used to force interactive mode. - `-json`: Show the output from `-buildinfo`, `-compileonly`, or `-version` in JSON. - `-log /path/to/log-file`: Path to a file to write debug logs to. - `-lsp`: Run the builtin language server. - `-norc`: Don't read the [RC file](#rc-file) when running [interactively](#using-elvish-interactively). The `-rc` flag is ignored if specified. - `-rc /path/to/rc`: Path to the [RC file](#rc-file) when running [interactively](#using-elvish-interactively). This can be useful for testing a new interactive configuration before installing it as your default config. - `-version`: Output the Elvish version and quit. See also `-buildinfo` and `-json`. ## Daemon flags The following flags are used by the storage daemon, a process for managing the access to the [database](#database-file). You shouldn't need to use these flags unless you are debugging daemon functionalities. - `-daemon`: Run the storage daemon instead of an Elvish shell. - `-db /path/to/db`: Path to the database file. This only has effect when used together with `-daemon`, or when there is no existing daemon running. - `-sock /path/to/socket`: Path to the daemon's UNIX socket. A non-daemon process will use this socket to send requests to the daemon, while a daemon process will listen on this socket. elvish-0.21.0/website/ref/doc.md000066400000000000000000000005201465720375400164030ustar00rootroot00000000000000 @module doc # Introduction The `doc:` module provides access to the documentation of Elvish modules. This module only supports builtin modules now, but will expand in future to cover user-defined modules too. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). elvish-0.21.0/website/ref/edit.md000066400000000000000000000503051465720375400165710ustar00rootroot00000000000000 @module 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 ref/edit/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 set edit:prompt = { tilde-abbr $pwd; put '> ' } set edit:rprompt = (constantly (styled (whoami)@(hostname) inverse)) ``` Some more examples: ```elvish-transcript ~> set edit:prompt = { tilde-abbr $pwd; styled '> ' green } ~> # ">" is now green ~> set 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. set edit:prompt-stale-transform = {|x| put $x } # Show stale prompts in inverse; equivalent to the default. set edit:prompt-stale-transform = {|x| styled $x inverse } # Gray out stale prompts. set 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 var n = 0 set edit:prompt = { sleep 2; put $n; set n = (+ $n 1); put ': ' } set edit:-prompt-eagerness = 10 # update prompt on each keystroke set 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 set 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 set 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 set 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 set 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 A key consists of a **key name**, preceded by zero or more **key modifiers**. Both are case-sensitive. The key name may be either: - A simple character, such as `x`. Most of the time you should use the lower-case form of letters, except for Ctrl- bindings, which usually require the upper-case form (so `Alt-x` but `Ctrl-X`). This quirk may go away in future. - 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 ``` Among these, `Tab` and `Enter` are equivalent to their ASCII counterparts `"\t"` and `"\n"`. Key modifiers are one of the following: ``` A Alt C Ctrl M Meta S Shift ``` Modifiers end with either a `-` or `+`, and can be stacked. Examples: - `S-F1` - `Ctrl-X` or - `Alt+Enter` - `C+A-X`. **Note:** The `Shift` modifier is only applicable to function keys such as `F1`. You cannot write `Shift-m` as a synonym for `M`. You may not actually be able to use the full range of possible keys for several reasons: - Some `Ctrl-[letter]` keys have special functions, like `Ctrl-C`, `Ctrl-Z`, `Ctrl-S` and `Ctrl-Z`. - Some `Ctrl-[letter]` keys are equivalent to single keypresses, like `Ctrl-J` (equivalent to `Enter`) and `Ctrl-I` (equivalent to `Tab`). - Keys involving multiple modifiers may not be supported by the terminal emulator, especially when the base key is a function key. ### Listing Modes The modes `histlist`, `location` 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 set 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 set edit:insert:binding[Ctrl-P] = { edit:history:start } set 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`]() 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 use re var all-packages = [(apt-cache search '' | re:awk {|0 1 @rest| put $1 })] set edit:completion:arg-completer[apt] = {|@args| var 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 use re 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)" | re:awk {|0 1 @rest| put $1 } } var common-git-commands = [ add branch checkout clone commit diff init log merge pull push rebase reset revert show stash status ] set edit:completion:arg-completer[git] = {|@args| var 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 set 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 set 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 set edit:before-readline = [{ echo 'going to read' }] set edit:after-readline = [{|line| echo 'just read '$line }] set 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. 2. At the very beginning of an Elvish session, or after a chunk of code is handled, `going to read` is printed. 3. 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`. ## Autofix The editor can identify **autofix** commands to fix some errors in the code. For example, if you try to use a command from [the `str:` module](str.html) without importing it, the editor will offer `use str` as an autofix command: ```ttyshot ref/edit/autofix ``` As seen above, autofixes are also applied automatically by [`edit:completion:smart-start`]() (the default binding for Tab) and [`edit:smart-enter`]() (the default binding for Enter). elvish-0.21.0/website/ref/edit/000077500000000000000000000000001465720375400162445ustar00rootroot00000000000000elvish-0.21.0/website/ref/edit/autofix-ttyshot.elvts000066400000000000000000000000231465720375400225110ustar00rootroot00000000000000~> #send-keys str: elvish-0.21.0/website/ref/edit/autofix-ttyshot.html000066400000000000000000000004301465720375400223220ustar00rootroot00000000000000~> str: elf@host Ctrl-A autofix: use str Tab Enter autofix first elvish-0.21.0/website/ref/edit/completion-mode-ttyshot.elvts000066400000000000000000000000671465720375400241350ustar00rootroot00000000000000~> cd elvish; echo '[CUT]' ~> #send-keys vim Space Tab elvish-0.21.0/website/ref/edit/completion-mode-ttyshot.html000066400000000000000000000012311465720375400237360ustar00rootroot00000000000000~/elvish> vim 1.0-release.md elf@host COMPLETING argument 1.0-release.md README.md syntaxes/ CONTRIBUTING.md SECURITY.md tools/ Dockerfile cmd/ vscode/ LICENSE go.mod website/ Makefile go.sum PACKAGING.md pkg/ elvish-0.21.0/website/ref/epm.md000066400000000000000000000105761465720375400164330ustar00rootroot00000000000000 @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`](). # 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.21.0/website/ref/file.md000066400000000000000000000003371465720375400165630ustar00rootroot00000000000000 @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.21.0/website/ref/flag.md000066400000000000000000000054371465720375400165630ustar00rootroot00000000000000 @module flag # Introduction The `flag:` module provides utilities for parsing command-line flags. This module supports two different conventions of command-line flags. The [Go convention](#go-convention) is recommended for Elvish scripts (and followed by the [Elvish command](command.html) itself). The alternative [getopt convention](#getopt-convention) is also supported, and useful for writing scripts that wrap existing programs following this convention. ## Go convention Each flag looks like `-flag=value`. For **boolean flags**, `-flag` is equivalent to `-flag=true`. For non-boolean flags, `-flag` treats the next argument as its value; in other words, `-flag value` is equivalent to `-flag=value`. Flag parsing stops before any non-flag argument, or after the flag terminator `--`. Examples (`-verbose` is a boolean flag and `-port` is a non-boolean flag): - In `-port 10 foo -x`, the `port` flag is `10`, and the rest (`foo -x`) are non flag arguments. - In `-verbose 10 foo -x`, the `verbose` flag is `true`, and the rest (`10 foo -x`) are non-flag arguments. - In `-port 10 -- -verbose foo`, the `port` flag is `10`, and the part after `--` (`-verbose foo`) are non-flag arguments. Using `--flag` is supported, and equivalent to `-flag`. **Note**: Chaining of single-letter flags is not supported: `-rf` is one flag named `rf`, not equivalent to `-r -f`. ## Getopt convention A flag may have either or both of the following forms: - A short form: a single character preceded by `-`, like `-f`; - A long form: a string preceded by `--`, like `--flag`. A flag may take: - No argument, like `-f` or `--flag`; - A required argument, like `-f value`, `-fvalue`, `--flag=value` or `--flag value`; - An optional argument, like `-f`, `-fvalue`, `--flag` or `--flag=value`. A short flag that takes no argument can be followed immediately by another short flag. For example, if `-r` takes no arguments, `-rf` is equivalent to `-r -f`. The other short flag may be followed by more short flags (if it takes no argument), or its argument (if it takes one). Assuming that `-f` and `-v` take no arguments while `-p` does, here are some examples: - `-rfv` is equivalent to `-r -f -v`. - `-rfp80` is equivalent to `-r -f -p 80`. Some aspects of the behavior can be turned on and off as needed: - Optionally, flag parsing stops after seeing the flag terminator `--`. - Optionally, flag parsing stops before seeing any non-flag argument. Turning this off corresponds to the behavior of GNU's `getopt_long`; turning it on corresponds to the behavior of BSD's `getopt_long`. - Optionally, only long flags are supported, and they may start with `-`. Turning this on corresponds to the behavior of `getopt_long_only` and the [Go convention](#go-convention). elvish-0.21.0/website/ref/index.toml000066400000000000000000000027201465720375400173240ustar00rootroot00000000000000autoIndex = true prelude = "prelude" [[articles]] name = "language" title = "Language specification" group = -1 [[articles]] name = "command" title = "The Elvish command" group = -1 [[groups]] intro = """ Modules in the Elvish standard library: """ [[articles]] name = "builtin" title = "Builtin functions and variables" [[articles]] name = "doc" title = "doc: Documentation of Elvish modules" [[articles]] name = "edit" title = "edit: API for the interactive editor" [[articles]] name = "epm" title = "epm: The Elvish Package Manager" [[articles]] name = "flag" title = "flag: Command-line flag parsing" [[articles]] name = "file" title = "file: File utilities" [[articles]] name = "math" title = "math: Math utilities" [[articles]] name = "md" title = "md: Markdown utilities" [[articles]] name = "os" title = "os: Operating system functionality" [[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 = "runtime" title = "runtime: Information about the Elvish runtime" [[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.21.0/website/ref/language.md000066400000000000000000002433011465720375400174270ustar00rootroot00000000000000 # 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: - `$`: introduces [variable use](#variable-use) - `*` and `?`: forms [wildcards](#wildcard-expansion) - `(` and `)`: encloses [output captures](#output-capture) - `[` and `]`: encloses [list](#list) or [map](#map) literals - `{` and `}`: encloses [lambda literals](#function) or [braced lists](#braced-list) - `<` and `>`: introduces [IO redirections](#redirection) - `;`: separates pipelines in a [code chunk](#code-chunk) - `|`: separates forms in a [pipeline](#pipeline); encloses [function](#function) signature - `&`: marks [background pipelines](#background-pipeline); introduces key-value pairs in [map literals](#map), [options](#ordinary-command), or [function](#function) signatures The following characters are parsed as metacharacters under certain conditions: - `~`: introduces [tilde expansion](#tilde-expansion) if appearing at the beginning of a compound expression **Note**: Not technically a metacharacter in this context, `~` is also used as a [variable suffix](#variable-suffix) to indicate variables for commands. - `=`: terminates [map keys](#map) and command option keys. **Note**: `:` is not technically a metacharacter, but is used in [qualified variable names](#qualified-name) and works as a [variable suffix](#variable-suffix) for namespaces. ## 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 (the ["U+" notation](https://en.wikipedia.org/wiki/Unicode#Architecture_and_terminology) represents Unicode codepoints in hexadecimal): - The following escape sequences represent some special characters: - `\a` is U+0007 BEL (bell). - `\b` is U+0008 BS (backspace). - `\t` is U+0009 HT (horizontal tabulation). - `\n` is U+000A LF (line feed), the standard line termination character on Unix. - `\v` is U+000B VT (vertical tabulation). - `\f` is U+000C FF (form feed). - `\r` is U+000D CR (carriage return). - `\e` is U+001B ESC (escape). - `\"` is U+0022, the double quote `"` itself. - `\\` is U+005C, the backslash `\` itself. - The following escape sequences encode any byte using their numeric values: - `\` followed by exactly three octal digits. - `\x` followed by exactly two hexadecimal digits. **Examples**: The character "A" (U+0041) is encoded using a single byte in UTF-8 (0x41), can be written as `\x41` or `\101`. The character "ß" (U+00DF) is encoded using two bytes in UTF-8 (0xc3 and 0x9f), and can be written as `\xc3\x9f` or `\303\237` (**not** as `\xdf` or `\337`). These notations can be used to write arbitrary byte sequences that are not necessary valid UTF-8 sequences. **Note**: `\0`, while supported by C, is invalid in Elvish; write `\x00` or `\000` instead. - The following escape sequences encode any Unicode codepoint using their numeric values: - `\u` followed by exactly four hexadecimal digits. - `\U` followed by exactly eight hexadecimal digits. **Examples**: The character "A" (U+0041) can be written as `\u0041` or `\U00000041`. The character "ß" (U+00DF) can be written as `\u00df` or `\U000000df`. - The following escape sequences encode ASCII control characters with the traditional [caret notation](https://en.wikipedia.org/wiki/Caret_notation): - `\^` followed by a single character between U+0040 and U+005F represents the codepoint that is 0x40 lower than it. For example, `\^I` is the tab character: 0x49 (`I`) - 0x40 = 0x09 (TAB). - `\^?` represents DEL (U+007F). - `\c` followed by character *X* is equivalent to `\^` followed by *X*. 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 will try to output filenames starting with `\`. # 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 ~> var 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 ~> var 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 ~> var 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 values that can be [indexed](#indexing) like maps, but do not support the full range of map operations. Pseudo-maps are usually values with special semantics in the Elvish runtime. The key-value pairs provide useful data about the value, but do not constitute the entirety of the value. Some examples of pseudo-maps are [exceptions](#exception) and [user-defined functions](#function). Pseudo-maps are printed like maps, but with a `^tag` immediately after the `[`, like `[^tag &key=value]`. This notation is a placeholder and is not valid syntax for constructing pseudo-map values. ## 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 in turn is also a pseudo-map in many cases, with 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. This list is not exhaustive, though. There are many error conditions that result in an opaque `reason` value that doesn't support introspection yet. 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] ``` Exceptions also carry stack traces. They are currently opaque values with no meaningful access methods yet, and will appear as `&stack-trace=<...>` when printing an exception value. When comparing whether two exceptions have the same cause, you should compare their reason fields (like `eq $e1[reason] $e2[reason]`). ## 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 ~> var 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 ~> var f = {|a b| put $b $a } ~> $f lorem ipsum ▶ ipsum ▶ lorem ``` 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 ~> var f = {|a @rest| put $a $rest } ~> $f lorem ▶ lorem ▶ [] ~> $f lorem ipsum dolar sit ▶ lorem ▶ [ipsum dolar sit] ~> set 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 ~> var 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 without quoting: - 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. A variable exist after it is declared using [`var`](#var), 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](#special-namespaces); 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. The default value is equivalent to the builtin [`nop`](builtin.html#nop) command. Such variables are consulted when resolving [ordinary commands](#ordinary-command) (for example, `foo` calls `$foo~`; see there for details). - If a variable name ends with `:`, it can only take namespaces as values. Such variables are consulted when evaluating variables with [qualified names](#qualified-name). ## 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 ~> var x = 12 ~> { echo $x } # $x is in the outer 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](builtin.html), 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 ``` ## Qualified name If a variable name contains a non-final `:`, it is called a **qualified name** and points to a variable in a namespace. (A final `:` is considered a [variable suffix](#variable-suffix) and such variables hold the namespaces themselves.) A qualified name is split after each non-final `:`, with the `:` attached to the component to the left. The first component is resolved like a normal variable, and subsequent components function like [indexing](#indexing). For example, `$a:b:c` is equivalent to `$a:[b:][c]`. **Note**: In future, namespace access may be subject to more static checking compared to indexing access. ## 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 function has returned. This is called [closure semantics](https://en.wikipedia.org/wiki/Closure_(computer_programming)), 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 { var n = 0 put { put $n } { set n = (+ $n 1) } } ~> var getter adder = (make-adder) ~> $getter # $getter outputs $n ▶ 0 ~> $adder # $adder increments $n ~> $getter # $getter and $setter refer to the same $n ▶ 1 ~> var getter2 adder2 = (make-adder) ~> $getter2 # $getter2 and $getter refer to different $n ▶ 0 ~> $getter ▶ 1 ``` ### Upvalues Variables that get "captured" in closures are called **upvalues**. 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 { var m = 2; var n = 3; put { put $n } } ~> var g = (f) ``` **Note**: The effect of this behavior is usually not noticeable, but has impacts on the [`eval`](builtin.html#eval) command. # 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 ~> var foo = bar ~> var 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 ~> var "\n" = foo ~> put $"\n" ▶ foo ~> var '!!!' = 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 ~> fn f { echo $x } compilation error: variable $x not found [tty 1], line 1: fn 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 ~> var 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 ~> var x = (+ 1 10 100) ~> put $x ▶ 111 ~> put lorem ipsum ▶ lorem ▶ ipsum ~> var 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**: Only the last newline is ever removed, so empty lines are preserved; `(echo "a\n")` evaluates to two values, `"a"` and `""`. **Note**: 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. **Note**: Output capture expressions do not introduce new scopes. For example, `nop (var x = foo)` will leave the variable `$x` defined. To introduce a new scope, wrap the code inside a [lambda](#function), e.g. `nop ({ var x = foo })`. ## 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 var output = (var error = ?(put foo; fail bad)) ``` ## 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 ~> var li = [foo bar] ~> put $li[0] ▶ foo ~> var 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 ~> var v = value ~> put '$v is '$v # compounding one string literal with one string variable ▶ '$v is value' ``` Among the types provided by the language, numbers are implicitly converted to strings in a compound expression, but other types require explicit conversions: ```elvish-transcript ~> var n = (num 10) ~> var l = [a b c] ~> echo 'Number: '$n Number: 10 ~> echo 'List: '$l Exception: cannot concatenate string and list [tty 18]:1:6: echo 'List: '$l ~> echo 'List: '(repr $l) List: [a b c] ``` When one or more of the constituent expressions evaluate to multiple values, the result is all possible combinations: ```elvish-transcript ~> var 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. Symbolic links are considered to be regular files. 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]`. Files that the Elvish runtime doesn't have appropriate access to are omitted silently. For example, if the runtime doesn't have appropriate access to either the `d` directory or the `d/y.cc` file, the result of `*/y.cc` may omit `d/y.cc`. ## 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 1]` 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) or a [special command](#special-command). Both 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 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 the variable `$head~` (where `head` is the value of the head) exists, it resolves to that variable. **Note**: Builtin commands and functions defined with [`fn`](#fn) are in fact variables ending with `~`. For example, the [`put`](builtin.html#put) command is stored in the `$put~` variable, and this mechanism allows you to call it as just `put`. Conversely, defining a variable like `var foo~ = { ... }` enables you to call it as either `$foo~` or `foo`. - 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. Examples of commands using static resolution: ```elvish-transcript ~> put x # resolves to builtin function $put~ ▶ x ~> var f~ = { put 'this is f' } ~> f # resolves to user-defined function $f~ ▶ 'this is f' ~> whoami # resolves to external command whoami elf ``` 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 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 ~> var x = /bin/whoami ~> $x elf ~> set 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). ## 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. It consists of three parts: - The **destination 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 can be omitted, in which case it is inferred from the operator. When the destination is given, it must precede the operator directly, without whitespaces in between. If there are whitespaces, Elvish will parse it as an argument instead. - The **operator** determines the mode to open files (if the source is a filename), and the destination if it is not explicitly specified. Possible redirection operators and their default destination ports 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). - The **source** can be one of the following: - A filename, in which case Elvish will open the named file to use for the destination port, using a suitable mode determined by the operator. - A file object, in which case it is used for the destination port. - A map, which works with one of two operators: - If the operator is `<`, the map must contain a file object in the `r` field, and that file is used as the redirection source. - If the operator is `>`, the map must contain a file object in the `w` field, and that file is used as the redirection source. - Other operators can't be used with maps. - The special syntax `&src` (where `src` is a number, or any of `stdin`, `stdout` and `stderr`) means duplicating the `src` port to the destination port. - The special syntax `&-` means closing the destination port. 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. ``` Examples for duplicating and closing ports: ```elvish-transcript ~> date >&- date: stdout: Bad file descriptor Exception: date exited with 1 [tty 3], line 1: date >&- ~> put foo >&- Exception: port does not support value output [tty 37], line 1: put foo >&- ``` IO ports modified by file redirections do not currently 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 ``` 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 previously referenced by a function. Example: ```elvish-transcript ~> var x = old ~> fn f { put $x } ~> var x = new ~> put $x ▶ new ~> f ▶ old ``` If the right-hand-side of the `var` command references the variable being shadowed, it sees the old variable: ```elvish-transcript ~> var x = foo ~> var x = [$x] # $x in RHS refers to old $x ~> put $x ▶ [foo] ``` ## Assigning 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 ~> set 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] ``` ## Assign temporarily: `tmp` {#tmp} The `tmp` command has the same syntax as [`set`](#set), and also requires all variables to already exist (use the [`var`](#var) special command to declare new variables). Unlike `var`, it saves the values of all variables before assigning them new values, and will restore them to the saved values when the current function has finished. The `tmp` command can only be used inside a function. Examples: ```elvish-transcript ~> var x = foo ~> fn f { echo $x } ~> { tmp x = bar; f } bar ~> f foo ``` ## Run with temporary assignment: `with` {#with} (Added in the 0.21 release series.) The `with` command has a similar syntax to [`set`](#set), but takes an additional lambda. It performs assignments, runs the lambda, and restores the variables to their original values. Example: ```elvish-transcript ~> var x = old ~> with x = new { echo $x } new ~> echo $x old ``` The `with` command also supports an alternative syntax where all assignment arguments are enclosed inside `[` and `]`. There can be multiple of them: ```elvish-transcript ~> var x y = old-x old-y ~> with [x = new-x] [y = new-y] { echo $x $y } new-x new-y ``` The same temporary assignment logic can usually be expressed with both `tmp` and `with`. For examples, the following are equivalent: ```elvish-transcript ~> var x = old ~> { tmp x = new; echo $x } ~> with x = new { echo $x } ``` Whether to use `tmp` or `with` is often a matter of style. ## 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 ~> var 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 ~> var '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 ~> var x = value ~> fn f { put $x } ~> del x ~> f ▶ value ``` Example of deleting map element: ```elvish-transcript ~> var m = [&k=v &k2=v2] ~> del m[k2] ~> put $m ▶ [&k=v] ~> var 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 use str fn tell-language {|fname| if (str:has-suffix $fname .go) { echo $fname" is a Go file!" } elif (str: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. **Note**: The `if` command itself doesn't introduce a new scope. For example, `if (var x = foo; put $x) { }` will leave the variable `$x` defined. However, the body blocks introduce new scopes because they are [lambdas](#function). ## 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). **Note**: The `while` command itself doesn't introduce a new scope. For example, `while (var x = foo; put $x) { }` will leave the variable `$x` defined. However, the body blocks introduce new scopes because they are [lambdas](#function). ## 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](#exception-capture) `?()` instead.) Syntax: ```elvish-transcript try { } catch exception-var { } else { } finally { } ``` This control structure behaves as follows: 1. The `try-block` is always executed first. 2. If `catch` is present, any exception that occurs in `try-block` is caught and stored in `exception-var`, and `catch-block` is then executed. Example: ```elvish-transcript ~> try { fail bad } catch e { put $e[reason] } ▶ [^fail-error &content=bad &type=fail] ``` If `catch` is not present, exceptions thrown from `try` are not caught: for instance, `try { fail bad } finally { echo foo }` will echo `foo`, but the exception is not caught and will be propagated further. **Note**: this keyword is spelt `except` in Elvish 0.17.x and before, but is otherwise the same. Using `except` still works in Elvish 0.18.x but is deprecated; it will be removed in Elvish 0.19.0. **Note**: the word after `catch` 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. Examples: ```elvish-transcript ~> try { fail bad } catch e { echo $e[reason] } else { echo good } [^fail-error &content=bad &type=fail] ~> try { nop } catch e { echo $e[reason] } else { echo good } good ``` Using `else` requires a `catch` to be present. The following code is invalid: ```elvish-transcript ~> try { nop } else { echo well } Compilation error: try with an else block requires a catch block [tty 1]:1:1-30: try { nop } else { echo 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 (that is, `catch` is not present), it is rethrown. At least one of `catch` and `finally` must be present: a lone `try { ... }` does not do anything on its own, and is almost certainly a mistake. To swallow exceptions, an explicit `catch` clause must be given. More examples with all possible clauses present: ```elvish-transcript ~> try { nop } catch e { put $e[reason] } else { put good } finally { put final } ▶ good ▶ final ~> try { fail bad } catch e { put $e[reason] } else { put good } finally { put final } ▶ [^fail-error &content=bad &type=fail] ▶ final ``` Exceptions thrown in blocks other than `try-block` are not caught. If an exception was thrown and either `catch-block` or `finally-block` throws another exception, the original exception is lost. Examples: ```elvish-transcript ~> try { fail bad } catch e { fail worse } Exception: worse Traceback: [tty], line 1: try { fail bad } catch e { fail worse } ~> try { fail bad } catch e { fail worse } finally { fail worst } Exception: worst Traceback: [tty], line 1: try { fail bad } catch 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 ▶ (num 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 does not support 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: - `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`. If the environment variable does not exist it expands to an empty string. **Note**: The `E:` namespace does not distinguish environment variables that are unset and those that are set but empty; for example, `eq $E:VAR ''` outputs `$true` if the `VAR` environment variable is either unset or empty. To make that distinction, use [`has-env`](./builtin.html#has-env) or [`get-env`](./builtin.html#get-env). **Note**: Unlike POSIX shells and the `e:` namespace, evaluation of variables do not fall back to the `E:` namespace; thus using `$E:...` (or [`get-env`](./builtin.html#get-env)) **is always needed** when expanding an environment variable. ## 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. 2. **User defined**: These match a [user defined module](#user-defined-modules) in a [module search directory](command.html#module-search-directories). 3. **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: - [builtin](builtin.html) - [edit](edit.html): 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): 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 have 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 your [`rc.elv`](command.html#rc-file): ```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. Note: to conditionally import a module into a REPL, see the [relevant section on `edit:add-var`](edit.html#conditionally-importing-a-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.21.0/website/ref/math.md000066400000000000000000000005331465720375400165730ustar00rootroot00000000000000 @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.21.0/website/ref/md.md000066400000000000000000000006221465720375400162410ustar00rootroot00000000000000 @module md # Introduction The `md:` module provides utilities for working with Markdown text. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). This module uses a custom implementation that supports [a large subset](https://pkg.go.dev/src.elv.sh/pkg/md#hdr-Which_Markdown_variant_does_this_package_implement_) of CommonMark. elvish-0.21.0/website/ref/os.md000066400000000000000000000022031465720375400162570ustar00rootroot00000000000000 @module os # Introduction The `os:` module provides access to operating system functionality. The interface is intended to be uniform across operating systems. The [builtin module](builtin.html) also contains some operating system utilities. The [`unix:` module](unix.html) contains utilities that are specific to UNIX operating systems. Function usages are given in the same format as in the reference doc for the [builtin module](builtin.html). ## The `&follow-symlink` option {#follow-symlink} Some commands take an `&follow-symlink` option, which controls the behavior of the command when the final element of the path is a symbolic link: - When the option is false (usually the default), the command operates on the symbolic link file itself. - When the option is true, the commands operates on the file the symbolic link points to. As an example, when `l` is a symbolic link to a directory: - `os:is-dir &follow-symlink=$false l` outputs `$false`, since the symbolic link file itself is not a directory. - `os:is-dir &follow-symlink=$true l` outputs `$true`, since the symlink points to a directory. elvish-0.21.0/website/ref/path.md000066400000000000000000000003571465720375400166020ustar00rootroot00000000000000 @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.21.0/website/ref/platform.md000066400000000000000000000001731465720375400174660ustar00rootroot00000000000000 @module platform # Introduction The `platform:` module provides access to the platform's identifying data. elvish-0.21.0/website/ref/prelude.md000066400000000000000000000003141465720375400172770ustar00rootroot00000000000000Reference documents also available as a docset. Download the [latest build](https://elv.sh/ref/docset/Elvish.tgz) or subscribe to the [feed](dash-feed://https%3A%2F%2Felv.sh%2Fref%2Fdocset%2FElvish.xml). elvish-0.21.0/website/ref/re.md000066400000000000000000000012351465720375400162500ustar00rootroot00000000000000 @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.21.0/website/ref/readline-binding.md000066400000000000000000000015241465720375400210360ustar00rootroot00000000000000 @module readline-binding # Introduction The `readline-binding` module provides GNU readline-like key bindings, such as binding Ctrl-A to move the cursor to the start of the line. GNU readline bindings are the default for shells such as Bash. So if you are migrating from Bash to Elvish you probably want to add the following to your [`rc.elv`](command.html#rc-file): ```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. The standard bindings are usually relocated to use Alt as the modifier -- the location mode is bound to Alt-L for example. See the [source code](https://src.elv.sh/pkg/mods/readline-binding/readline-binding.elv) for details. elvish-0.21.0/website/ref/runtime.md000066400000000000000000000001641465720375400173250ustar00rootroot00000000000000 @module runtime # Introduction The `runtime:` module provides information about the Elvish runtime. elvish-0.21.0/website/ref/store.md000066400000000000000000000002421465720375400167730ustar00rootroot00000000000000 @module store # Introduction The `store:` module provides access to Elvish's persistent data store. It is only available in interactive mode now. elvish-0.21.0/website/ref/str.md000066400000000000000000000005421465720375400164520ustar00rootroot00000000000000 @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). The builtin module also contains some string utilities, such as string comparison commands like [` @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.21.0/website/reset.css000066400000000000000000000027771465720375400164140ustar00rootroot00000000000000/* 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.21.0/website/sgr.css000066400000000000000000000034441465720375400160550ustar00rootroot00000000000000/* SGR classes used in highlighted code blocks and ttyshots. */ .sgr-1 { /* * Bold text is wider than regular text in most fonts, and can break * vertical alignment in ttyshots. Emulate bold font with text-shadow. */ text-shadow: 0.05em 0 0; } .sgr-4 { text-decoration: underline; } /* * SGR 7 (inverse) has some special handling by the ttyshot program; see * comments there. */ .sgr-7fg { color: white; } .sgr-7bg { background-color: black; } .dark .sgr-7fg { color: black; } .dark .sgr-7bg { background-color: #eee; } /* black */ .sgr-30 { color: black; } .sgr-40 { background-color: black; } /* red */ .sgr-31 { color: maroon; } .sgr-41 { background-color: maroon; } /* green */ .sgr-32 { color: green; } .sgr-42 { background-color: green; } /* yellow */ .sgr-33 { color: goldenrod; } .sgr-43 { background-color: goldrenrod; } /* blue */ .sgr-34 { color: navy; } .sgr-44 { background-color: navy; } /* magenta */ .sgr-35 { color: darkorchid; } .sgr-45 { background-color: darkorchid; } /* cyan */ .sgr-36 { color: darkcyan; } .sgr-46 { background-color: darkcyan; } /* white */ .sgr-37 { color: lightgrey; } .sgr-47 { background-color: lightgrey; } /* bright black */ .sgr-90 { color: grey; } .sgr-100 { background-color: grey; } /* bright red */ .sgr-91 { color: red; } .sgr-101 { background-color: red; } /* bright green */ .sgr-92 { color: lime; } .sgr-102 { background-color: lime; } /* light yellow */ .sgr-93 { color: yellow; } .sgr-103 { background-color: yellow; } /* light blue */ .sgr-94 { color: blue; } .sgr-104 { background-color: blue; } /* bright magenta */ .sgr-95 { color: fuchsia; } .sgr-105 { background-color: fuchsia; } /* bright cyan */ .sgr-96 { color: aqua; } .sgr-106 { background-color: aqua; } /* bright white */ .sgr-97 { color: white; } .sgr-107 { background-color: white; } elvish-0.21.0/website/slides/000077500000000000000000000000001465720375400160265ustar00rootroot00000000000000elvish-0.21.0/website/slides/2023-03-london-gophers.md000066400000000000000000000117241465720375400221170ustar00rootroot00000000000000# Elvish An expressive, versatile cross-platform shell implemented in Go Qi Xiao (xiaq) 2023-03-22 @ London Gophers *** # Intro - Software engineer at Google - I don't speak for my employer, etc. etc. - Started Elvish in 2013 *** # Why build a shell - I use a shell every day I use a computer - Traditional shells aren't good enough - Want a shell with - Nice interactive features - Serious programming constructs *** # Why not build a shell - Too hard for users to switch - Hopefully overcomeable - "Shells are *supposed* to be primitive and arcane" - Defeatism *** # Part 1: # Elvish as a shell and programming language *** # A better shell - Enhanced interactive features - Location mode (Ctrl-L) - Navigation mode (Ctrl-N) - This works like a traditional shell (only new feature is `**`): ```elvish cat **.go | grep -v '^$' | wc -l ``` *** # Somewhat new takes - Get command output with `()`: ```elvish wget dl.elv.sh/(go env GOGOS)-(go env GOARCH)/elvish-HEAD ``` - Variables must be declared: ```elvish var lorem = foo echo $lorme # Error! ``` - Variable values are never split: ```elvish var x = 'foo bar' touch $x ``` *** # Data structures - Lists: `[foo bar]` - Maps: `[&foo=bar]` - Index them: ```elvish var l = [foo bar]; echo $l[0] var m = [&foo=bar]; echo $m[foo] ``` - Nest them: ```elvish var l = [[foo] [&[foo]=[bar]]] echo $l[1][[foo]] ``` *** # Data structures in "shell use cases" - Automation with multiple parameters: ```elvish for host [[&name=foo &port=22] [&name=bar &port=2200]] { scp -P $host[port] elvish root@$host[name]:/usr/local/bin/ } ``` - Abbreviations are maps: ```elvish set edit:abbr[xx] = '> /dev/null' ``` - Better programming language leads to better shell *** # Lambdas! - Lambdas: ```elvish var f = {|x| echo 'Hello '$x } $f world var g = { echo 'Hello' } # Omit empty arg list $g ``` *** # Lambdas in "shell use cases" - Prompts are lambdas: - ```elvish set edit:prompt = { put (whoami)@(tilde-abbr $pwd)'> ' } ``` - No mini-language like `PS1='\u@\w> '` - Configure command completion with a map to lambdas: ```elvish set edit:completion:arg-completer[foo] = {|@x| echo lorem echo ipsum } ``` *** # Outputting and piping values - Arithmetic: ```elvish * 7 (+ 1 5) ``` - String processing: ```elvish use str for x [*.jpg] { gm convert $x (str:trim-suffix $x .jpg).png } ``` - Stream processing: ```elvish put [x y] [x] | count put [x y] [x] | each {|v| put $v[0] } ``` *** # Conclusions of part 1 - Familiar shell - With a real programming language - Better programming language -> better shell - Many features not covered - Environment variables, exceptions, user-defined modules, ... *** # Part 2: # Elvish as a Go project - Implementation - Experience of using Go - CI/CD practice *** # Implementation overview - Frontend (parser) - Hand written parser - Interface and recursion - `type Node interface { parse(*parser) }` - Backend (interpreter) - Compile the parse tree into an op tree - Interface and recursion - `type effectOp interface { exec(*Frame) Exception }` - Terminal UI - Arcane escape codes *** # Notable Go features in Elvish's runtime - Running external commands: `os.StartProcess` - Pipelines: goroutines, `sync.WaitGroup` - Outputting and piping values: `chan any` - Big numbers: `math/big` - Go standard library - Elvish's `str:trim-suffix` is just Go's `strings.TrimSuffix`, etc. - Reflection-based binding *** # Why Go? - Reasonably performant - Suitable runtime (goroutines, GC) - Fast compilation and easy cross-compilation - Rust wasn't released yet *** # Wishlist - Nil safety - Plugin support on more platforms - Faster reflection *** # Experience over the years - Go 1.1 (!) was the latest version when I started Elvish - Relatively few changes over the years - 1.5: vendoring - 1.11: modules - 1.13: `-trimpath` - 1.16: `//go:embed` - 1.18: Generics, fuzzing *** # CI - GitHub Actions - Tests, go vet, staticcheck, etc. - Uploading test coverages to codecov.io - Cirrus CI for more platforms - {Free Net Open}BSD - Linux ARM64 - Both are free for Elvish's current use cases *** # Website and prebuilt binaries - - Webhook - Building the website - A custom CommonMark implementation - A custom static site generator - Building the binaries - Reproducible - Verified by both CI environments - Two nodes globally, with geo DNS - ~ $20 per month (VPS + domain name + DNS) *** # Learn more elvish-0.21.0/website/slides/2024-08-rc-design.md000066400000000000000000000171361465720375400210450ustar00rootroot00000000000000# Designing a shell language for the 2010s Qi Xiao (xiaq) 2024-08-01 @ Recurse Center *** # Design principles - Starting point - A modern general-purpose language - ... inheriting the *spirit* and *feel* of traditional shells - ⇒ A language that can scale... - down, to one-liners - up, to moderately complex projects - My design decisions - *One* scalable language - No separate features for interactive use vs scripting - No separate command vs expression syntax - Modern, but not experimental - Use established ideas (hence the 2010s) - But maybe combine them in new ways *** # What's the spirit of the shell anyway? - The original scripting language - Old dichotomy - System languages: compiled, statically typed, optimized for machine speed - Scripting languages: interpreted, dynamically typed, optimized for human speed - Boundaries have blurred - A more essential identity - Highly interactive: quick feedback loop, inspectable runtime - Good at interfacing the outside world - programs written by other people, data from elsewhere *** # What does the language look like? - ```elvish # jpg-to-png.elv for x [*.jpg] { gm convert $x (str:trim-suffix $x .jpg).png } ``` - ```elvish # update-servers-in-parellel.elv var hosts = [[&name=a &cmd='apt update'] [&name=b &cmd='pacman -Syu']] # peach = "parallel each" peach {|h| ssh root@$h[name] $h[cmd] } $hosts ``` - [Detailed explainers](https://elv.sh/learn/scripting-case-studies.html) *** # Inheriting traditions - Barewords are strings ```elvish vim design.md echo $pid # Variables need $ ``` - Uniform, concise command syntax ```elvish cd d # A builtin command vim design.md # An external command if (eq a b) { ... } # A control-flow command ``` - Information flows via input and output ```elvish cat *.go | wc -l # Either via pipeline mkdir (date +%Y-%m-%d) # Or via output capture ``` *** # Evolving traditions to suit a modern language

    - Just strings → Proper data structures ```elvish var str = foo var list = [lorem [ipsum dolar]] var map = [&key=[&key=value]] ``` - Text output → Value and text output (similar to PowerShell) ```elvish-transcript ~> put foo [lorem [ipsum dolar]] ▶ foo ▶ [lorem [ipsum dolar]] ``` - Text pipeline → Value and text pipe ```elvish-transcript ~> put foobar "a\nb" | order ▶ "a\nb" ▶ foobar ```
    - Exit code → Exception ```elvish-transcript ~> fail 'oh no' Exception: oh no [tty 36]:1:1-12: fail 'oh no' ~> false Exception: false exited with 1 [tty 12]:1:1-5: false ```
    *** # Functional programming - Traditional shells have FP in a sense... ```bash # Bash code trap 'echo exiting' EXIT PS1='$(git_prompt)' ``` - Elvish has real lambdas, which look like `{ code }` or `{|arg| code}` - Once you have them, they pop up everywhere ```elvish peach {|h| ssh root@$h 'apt update'} [server-a server-b] time { sleep 1s } if (eq (uname) Darwin) { echo 'Hello macOS user!' } set edit:prompt = { print (whoami)@(tilde-abbr $pwd)'$ ' } ``` *** # Functional programming with immutable data
    - Immutable data, not stateful objects - Strings, lists, maps are all immutable - Make new values based on old values instead: ```elvish-transcript ~> conj [a b] c ▶ [a b c] ~> assoc [a b] 0 x ▶ [x b] ~> assoc [&k1=v1 &k2=v2] k1 new-v1 ▶ [&k1=new-v1 &k2=v2] ``` - Lists and maps are based on [hash array mapped tries](https://en.wikipedia.org/wiki/Hash_array_mapped_trie)
    - Variables (and variables alone) are mutable ```elvish var v = old set v = new ``` - "Mutating" lists and maps ```elvish-transcript ~> var li = [a b] ~> var li2 = $li ~> set li2[0] = x ~> put $li $li2 ▶ [a b] ▶ [x b] ```
    *** # Functional programming with a concatenative twist - Pipelines model dataflows naturally - Nested functional calls: ```elvish var evens = [(keep-if {|x| == 0 (% $x 2)} [(range 100)])] ``` - Concatenative: ```elvish range 100 | keep-if {|x| == 0 (% $x 2)} | var evens = [(all)] ``` - We also get concurrency *** # Namespaces and modules - Builtin modules: ```elvish use str str:has-prefix foobar f ``` - Environment variables: ```elvish echo $E:LSCOLORS set E:http_proxy = ... ``` - User-defined modules ```elvish # ~/.config/elvish/lib/a.elv var foo = bar # in shell session use a echo $a:foo ``` - Local names are checked statically ```elvish set foo = bar # compilation error if foo is not declared ``` *** # Typing - Dynamic typing - Scale down - Interface with external data - Strong typing - With some pockets of weak typing: ```elvish-transcript ~> + 1 2 ▶ (num 3) ~> + (num 1) (num 2) ▶ (num 3) ``` - Potentially problematic: ```elvish var n = 0 if (...) { set n = (+ $n 1) } ``` - Static typing? - Gradual typing and structural typing are essential - TypeScript has got a lot right (but is way too big) *** # Various bits I like - Lisp-1 vs Lisp-2: ```elvish echo a # resolves to $echo~ var foo~ = { echo foo } foo ``` - Arbitrary-precision numbers (R6RS numerical tower) ```elvish-transcript ~> * (range 1 40) ▶ (num 20397882081197443358640281739902897356800000000) ~> + 1/10 1/5 ▶ (num 3/10) ``` - Concatenation instead of interpolation: ```elvish echo 'Hello, '(whoami) ``` *** # What's next - TUI framework - Improve CI/CD experience with Elvish - Language features to figure out - Modelling behavior of types - Making exceptions work - Dependency management - Type system - The logo *** # Cultural shifts - Elvish was started in 2013 - Cultural changes in shell user community - Skepticism for new shells has become less common - "I'd rather just use Python/Ruby/...", because - "I like my shell to be dumb" - "I already know bash/zsh" - The former has become less common over the years - Changes in programming culture - C++/Java-style OOP is no longer canon - Mutable objects are giving way to immutable data - Inheritance has given way to composition and interfaces - Exceptions have become less popular - Gradual typing is now mainstream (implementations can still be hit-and-miss) - Bar for tooling has risen (dependency management, editor support, ...) *** # Review - What are unusual about Elvish all come from predecessors - Traditional shell: syntax; data flow with pipelines and output capture - PowerShell: IO can carry not just text - Clojure: immutable data structures - The result is a functional scripting language with a unique style - The rest is "just" general language design - With some unique constraints - And an ever-shifting goalpost *** # Q&A elvish-0.21.0/website/slides/2024-08-rc-implementation.md000066400000000000000000000457461465720375400226310ustar00rootroot00000000000000# Implementation of Elvish # Or, how to write a programming language and shell in Go with 92% test coverage and instant CI/CD Qi Xiao (xiaq) 2024-08-02 @ Recurse Center *** # Agenda - What's Elvish? - Implementing the interpreter - Testing the interpreter - Testing the terminal app - Miscellaneous bits *** # What's Elvish? *** # Elvish is a modern shell - What's a shell? - A programming language and terminal app - ... that helps you interface with your operating system - Traditional shells: Bourne sh, csh, ksh, dash, bash, zsh - What's a modern shell? - Full-fledged programming language - More powerful interactive features - Other modern shells: [Nushell](https://www.nushell.sh), [Oils](https://www.oilshell.org), [Murex](https://murex.rocks) *** # Traditional shell language features - Run external commands: ```elvish vim main.go ``` - Wildcards, text pipelines: ```elvish cat *.go | wc -l # Elvish also supports recursive wildcards cat **.go | wc -l ``` - Capturing output of commands: ```elvish # Like $(whoami) or `whoami` in traditional shells echo 'Greetings, '(whoami)'!' ``` *** # Modern language features - Lists: ```elvish var list = [foo bar [lorem ipsum]] echo $list[2][0] ``` - Maps: ```elvish var speed = [&Go=[&compile=fast &run=OK] &Rust=[&compile=slow &run=fast]] echo 'Go is '$speed[Go][compile]' to compile' ``` - Value outputs and pipelines: ```elvish-transcript ~> str:split , 'Rob Pike,Ken Thompson,Robert Griesemer' ▶ 'Rob Pike' ▶ 'Ken Thompson' ▶ 'Robert Griesemer' ~> str:split , 'Rob Pike,Ken Thompson,Robert Griesemer' | str:join '|' ▶ 'Rob Pike|Ken Thompson|Robert Griesemer' ``` *** # Functional programming - Lambdas look like `{ code }` or `{|arg| code}` - Once you have them, they pop up everywhere ```elvish # Using a lambda for parallel execution peach {|h| ssh root@$h 'apt update'} [server-a server-b] # Timing a lambda time { sleep 1s } # The if-body is a lambda if (eq (uname) Darwin) { echo 'Hello macOS user!' } # Using a lambda to define the prompt set edit:prompt = { print (whoami)@(tilde-abbr $pwd)'$ ' } ``` *** # Interactive features - (Demo) - Syntax highlighting - Completion with Tab - Directory history with Ctrl-L - Command history with Ctrl-R - Filesystem navigator with Ctrl-N *** # Implementing the Elvish interpreter *** # Interpreter overview - An interpreter works in stages: 1. Parse: code → **syntax tree** Each node contains syntactical information 2. Compile: syntax tree → **op tree** Each node contains information for execution, and has an `exec` method 3. Execute: Call `exec` on op tree root - Every stage is divide-and-conquer on a tree structure - (A variant of tree walker) *** # An example - Source code ```elvish echo $pid | wc ```
    - Syntax tree: ![AST](./2024-08-rc-implementation/syntax-tree.svg)
    - Op tree: ![Op tree](./2024-08-rc-implementation/op-tree.svg)
    *** # Parsing - Parser state ![parser state](./2024-08-rc-implementation/parser-state.svg) - Parser operations - *Peek*: look at what `next` points to - *Consume*: move `next` forward - How do I parse a `Pipeline`? - Divide and conquer: to parse a `Pipeline`, parse a `Form`, and if we see `|`, consume it and parse another `Form` - How do I parse a `Form`? - Divide and conquer: to parse a `Form`, parse an `Expr` (the head), and as long as we don't see `|` or newline, parse another `Expr` (an argument) - How do I parse an `Expr`? - ... *** # Parsing a pipeline ```go type parser struct { src string; next int } type Pipeline struct { Forms []Form } func parsePipeline(ps *parser) Pipeline { // To parse a Pipeline, forms := []Form{parseForm(ps)} // parse a Form, for ps.peek() == '|' { // as long as we see '|', ps.next++ // consume it, forms = append(forms, parseForm(ps)) // and parse another Form } return Pipeline{Forms} } ``` - [Real code](https://github.com/elves/elvish/blob/d8e2284e61665cb540fd30536c3007c4ee8ea48a/pkg/parse/parse.go#L132) *** # Compiling a pipeline ```go type pipelineOp struct { formOps []formOp } func (cp *compiler) compilePipeline(p Pipeline) pipelineOp { formOps := make([]formOp, len(p.Forms)) for i, f := range p.Forms { formOps[i] = compileForm(f) } return pipelineOp{formOps} } ``` - Similarly, divide and conquer - Compilation of other nodes is more interesting - [Real code](https://github.com/elves/elvish/blob/d8e2284e61665cb540fd30536c3007c4ee8ea48a/pkg/eval/compile_effect.go#L46) *** # Execution - The `exec` method is where the real action happens ```go type pipelineOp struct { formOps []formOp } func (op *pipelineOp) exec() { /* ... */ } type formOp struct { /* ... */ } func (op *formOp) exec() { /* ... */ } ``` - ```elvish echo $pid | wc ``` How do we connect the output of `echo` to the input of `wc`? - You exist in the context of all in which you live ```go type Context struct { stdinFile *os.File; stdinChan <-chan any stdoutFile *os.File; stdoutChan chan<- any } func (op *pipelineOp) exec(*Context) { /* ... */ } func (op *formOp) exec(*Context) { /* ... */ } ``` *** # Executing a pipeline ```go type pipelineOp struct { forms []formOp } func (op *pipelineOp) exec(ctx *Context) { form1, form2 := forms[0], forms[1] // Assume 2 forms r, w, _ := os.Pipe() // Byte pipeline ch := make(chan any, 1024) // Channel pipeline ctx1 := ctx.cloneWithStdout(w, ch) // Context for form 1 ctx2 := ctx.cloneWithStdin(r, ch) // Context for form 2 var wg sync.WaitGroup // Now execute them in parallel! wg.Add(2) go func() { form1.exec(ctx1); wg.Done() }() go func() { form2.exec(ctx2); wg.Done() }() wg.Wait() } ``` - [Real code](https://github.com/elves/elvish/blob/d8e2284e61665cb540fd30536c3007c4ee8ea48a/pkg/eval/compile_effect.go#L69) *** # Reviewing the example - Source code ```elvish echo $pid | wc ```
    - Syntax tree: ![AST](./2024-08-rc-implementation/syntax-tree.svg)
    - Op tree: ![Op tree](./2024-08-rc-implementation/op-tree.svg)
    *** # Go is great for writing a shell - Pipeline semantics - Text pipelines: [`os.Pipe`](https://pkg.go.dev/os#Pipe) - Value pipelines: channels - Concurrent execution: Goroutines and [`sync.WaitGroup`](https://pkg.go.dev/sync) - Running external commands: [`os.StartProcess`](https://pkg.go.dev/os#StartProcess) *** # Go is great for writing an interpreted language - Rich standard library - Big numbers ([`big.Int`](https://pkg.go.dev/math/big#Int) and [`big.Rat`](https://pkg.go.dev/math/big#Rat)): ```elvish-transcript ~> * (range 1 41) # 40! ▶ (num 815915283247897734345611269596115894272000000000) ~> + 1/10 2/10 ▶ (num 3/10) ``` - [`math`](https://pkg.go.dev/math), [`strings`](https://pkg.go.dev/strings) (`str:` in Elvish), [`regexp`](https://pkg.go.dev/regexp) (`re:` in Elvish): ```elvish-transcript ~> math:log10 100 ▶ (num 2.0) ~> str:has-prefix foobar foo ▶ $true ~> re:match '^foo' foobar ▶ $true ``` - Garbage collection comes for free! *** # Testing the Elvish interpreter *** # How do you get to 90%+ test coverage? - Make writing tests *really* easy - Interpreters have a super simple API! - Input: code - Output: text, values *** # Iteration 1: table-driven tests ```go var tests = []struct{ code string wantValues []any wantBytes string }{ {code: "echo foo", wantBytes: "foo\n"}, } func TestInterpreter(t *testing.T) { for _, test := range tests { gotValues, gotBytes := Interpret(test.code) // Compare with test.wantValues and test.wantBytes } } ``` *** # Adding a test case with table-driven tests - Steps: 1. Implement new functionality 2. Test manually in terminal: ```elvish-transcript ~> str:join , [a b] ▶ 'a,b' ``` 3. Convert the interaction into a test case: ```go {code: "str:join , [a b]", wantValues: []any{"a,b"}} ``` - Step 3 can get repetitive - What if we let the computer do the conversion? 🤔 *** # Iteration 2: transcript tests - Record terminal *transcripts* in `tests.elvts`: ```elvish-transcript ~> str:join , [a b] ▶ 'a,b' ``` - Generate the table from the terminal transcript: ```go //go:embed tests.elvts const transcripts string func TestInterpreter(t *testing.T) { tests := parseTranscripts(transcripts) for _, test := range tests { /* ... */ } } ``` *** # Adding a test case with transcript tests - Steps: 1. Implement new functionality 2. Test manually in terminal: ```elvish-transcript ~> str:join , [a b] ▶ 'a,b' ``` 3. Copy the terminal transcript into `tests.elvts` - Copying is still work - What if we don't even need to copy? 🤔 *** # Iteration 2.1: an editor extension for transcript tests - Editor extension for `.elvts` files - Run code under cursor - Insert output below cursor - Steps (demo): 1. Implement new functionality 2. Test manually in `tests.elvts` within the editor: ```elvish-transcript ~> use str ~> str:join , [a b] ▶ 'a,b' ``` *** # Testing the terminal app *** # Widget abstraction - Like GUI apps, Elvish's terminal app is made up of *widgets* ```go type Widget interface { Handle(event Event) Render(width, height int) *Buffer } ``` - `Buffer`: stores *rich text* and the cursor position - `Event`: keyboard events (among others) - Example: `CodeArea` - Stores text content and cursor position - `Render`: writes a `Buffer` with current content and cursor - `Handle`: - a → insert `a` - Backspace → delete character left of cursor - Left → move cursor left *** # Widget API is also simple(-ish) - Input: `Event` - Output: `Buffer` - But: - Multiple inputs and outputs, often interleaved. A typical test: 1. Press x, press y, render and check 2. Press Left, render and check 3. Press Backspace, render and check - Tests end up verbose and not easy to write 😞 *** # Leveraging Elvish and transcript tests!
    - Create Elvish bindings for the widget - Now just use Elvish transcript tests ```elvish-transcript ~> send-keys [x y]; render xy ~> send-keys [Left]; render xy ~> send-keys [Backspace]; render y ```
    Actual output of `render` is slightly more sophisticated... ```elvish-transcript ~> send-keys [x y]; render ┌────────────────────────────────────────┐ │xy │ │ ̅̂ │ └────────────────────────────────────────┘ ~> send-keys [Left]; render ┌────────────────────────────────────────┐ │xy │ │ ̅̂ │ └────────────────────────────────────────┘ ~> send-keys [Backspace]; render ┌────────────────────────────────────────┐ │y │ │ ̅̂ │ └────────────────────────────────────────┘ ```
    *** # Miscellaneous bits *** # Continuous deployment ![Continuous deployment pipeline](./2024-08-rc-implementation/cd.svg) - Go is a great language to write a web server with - Elvish is a great language for scripting - *** # Personal perspectives - Project goes back to 2013 (Go 1.1) - Go 1.1 was already a solid language for writing Elvish - Modules (1.11), generics and fuzzing (1.18) were more impactful changes - But I wish Go has... - Nil safety - Plugin support on more platforms (for native modules) - Interpreter is sophisticated but not a lot of code - Focuses on simplicity, not very performant - The terminal app is a *lot* of code *** # More about Elvish - Website: - Get Elvish: ```elvish curl -so - https://dl.elv.sh/darwin-arm64/elvish-HEAD.tar.gz | sudo tar -xzvC /usr/local/bin ``` No need for `curl ... | sh`, it's literally one binary :) - GitHub repo: Developer docs: *** # Q&A elvish-0.21.0/website/slides/2024-08-rc-implementation/000077500000000000000000000000001465720375400222675ustar00rootroot00000000000000elvish-0.21.0/website/slides/2024-08-rc-implementation/cd.svg000066400000000000000000000146501465720375400234040ustar00rootroot00000000000000 continuous_deployment cluster_0 My £5/mo VPS webhook_listener Webhook listener (Go) scripts Scripts (Elvish) webhook_listener->scripts filesystem File- system scripts->filesystem caddy Caddy caddy->filesystem dev User github GitHub dev->github push user Dev user->caddy https://elv.sh github->webhook_listener webhook elvish-0.21.0/website/slides/2024-08-rc-implementation/op-tree.svg000066400000000000000000000125671465720375400243760ustar00rootroot00000000000000 optree Pipeline pipelineOp Form echo formOp Pipeline->Form echo Form Form wc formOp Pipeline->Form wc Form Expr echo variableOp Scope= Builtin Name= "echo~" Form echo->Expr echo Head Expr $pid variableOp Scope= Builtin Name="pid" Form echo->Expr $pid Arg Expr wc literalOp Value= ExternalCmd{"wc"} Form wc->Expr wc Head elvish-0.21.0/website/slides/2024-08-rc-implementation/parser-state.svg000066400000000000000000000111721465720375400254240ustar00rootroot00000000000000 parser a e b c c h d o e f $ g p h i i d j k | l m w n c next next next->a elvish-0.21.0/website/slides/2024-08-rc-implementation/syntax-tree.svg000066400000000000000000000116331465720375400252770ustar00rootroot00000000000000 AST Form echo Form Expr echo Expr Type=Bareword Value="echo" Form echo->Expr echo Head Expr $pid Expr Type=Variable Value="pid" Form echo->Expr $pid Arg Form wc Form Expr wc Expr Type=Bareword Value="wc" Form wc->Expr wc Head Pipeline Pipeline Pipeline->Form echo Form Pipeline->Form wc Form elvish-0.21.0/website/slides/Makefile000066400000000000000000000001661465720375400174710ustar00rootroot00000000000000MDS := $(wildcard *.md) HTMLS := $(MDS:.md=.html) all: $(HTMLS) %.html: %.md gen.elv template.html ./gen.elv $< $@ elvish-0.21.0/website/slides/gen.elv000077500000000000000000000006021465720375400173100ustar00rootroot00000000000000#!/usr/bin/env elvish use str use flag fn main { |&title=Presentation md html| var content = (go run src.elv.sh/website/cmd/md2html < $md | slurp) slurp < template.html | str:replace '$common-css' (cat ../reset.css ../sgr.css | slurp) (one) | str:replace '$title' $title (one) | str:replace '$content' $content (one) | print (one) > $html } flag:call $main~ $args elvish-0.21.0/website/slides/template.html000066400000000000000000000120711465720375400205300ustar00rootroot00000000000000 $title
    ? / ?
    $content
    elvish-0.21.0/website/sponsor/000077500000000000000000000000001465720375400162465ustar00rootroot00000000000000elvish-0.21.0/website/sponsor/index.toml000066400000000000000000000000571465720375400202540ustar00rootroot00000000000000prelude = "prelude" extraCSS = ["prelude.css"] elvish-0.21.0/website/sponsor/prelude.css000066400000000000000000000005771465720375400204310ustar00rootroot00000000000000.action { display: flex; column-gap: 16px; } .action a { width: fit-content; height: 40px; padding: 0 16px; border: 1px solid; border-radius: 5px; font-family: var(--sans-font); color: white; display: flex; justify-content: center; align-items: center; } .action a.github { background: #333; } .action a.patreon { background: var(--deep-orange-700); } elvish-0.21.0/website/sponsor/prelude.md000066400000000000000000000011461465720375400202320ustar00rootroot00000000000000Elvish is free software under a [permissive BSD license](https://github.com/elves/elvish/blob/master/LICENSE). However, developing free software is not free. Elvish is a project exploring the boundary of what shells can do, and developing it takes considerable time, energy and creativity. To make the project more sustainable, please sponsor Elvish's creator and main developer, `xiaq`: elvish-0.21.0/website/style.css000066400000000000000000000305511465720375400164210ustar00rootroot00000000000000/** * Global styling. **/ * { box-sizing: border-box; } :root { --code-font: "Fira Mono", Menlo, "Roboto Mono", Consolas, monospace; --sans-font: Helvetica, Arial, sans-serif; --link-color-on-white: #0645ad; --link-color-on-black: #accbff; /* Colors from 2014 Material Design color palettes (https://m2.material.io/design/color/the-color-system) */ --blue-800: #1565C0; --deep-orange-500: #FF5722; --deep-orange-700: #E64A19; } 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.4; } body.has-js .no-js, body.no-js .has-js { display: none !important; } /* Firefox allows the user to specify colors in the UA stylesheet, so set them explicitly. */ body { color: black; background: white; } body.dark { color: #eee; background: black; } /** * Top-level layout. * * There are two main elements: #navbar and #main. 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: 7px 0; } #navbar, #main { /* Keep the content at 800px. */ max-width: 832px; margin: auto; padding: 0 16px; } /* 832px = max-width + left and right padding of #main. After this screen width, #main will no longer get wider, but we allow .extra-wide elements to continue to get wider up to 900px, using negative left and right margins. */ @media screen and (min-width: 832px) { .extra-wide { /* 32px is left and right padding of #main. */ width: calc(min(100vw - 32px, 900px)); /* upper bound is calculated by substituting 100vw = 900px + 32px */ margin-inline: calc(max((832px - 100vw) / 2, -50px)); } } /** * Elements in the navbar. * * The navbar is made up of two elements, #site-title and ul#nav-list. The * latter contains
  • s, each of which contains an . */ #site-title, #nav-list { display: inline-block; /* Add spacing between lines when the navbar cannot fit in one line. */ line-height: 1.4; } #site-title { /* Move the title downward 1px so that it looks more aligned with the * category list. */ position: relative; top: 1px; font-size: 1.2em; font-family: var(--code-font); } #site-title { color: #5b5; } #nav-list { /* Override the margins for