├── .github └── FUNDING.yml ├── .gitignore ├── .golangci.yml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── build.oh ├── check.oh ├── doc.oh ├── doctest │ ├── 000-continuations-test.oh │ ├── 000-dynamic-test.oh │ ├── 000-exceptions-test.oh │ ├── 000-homoiconic-test.oh │ ├── 000-local-test.oh │ ├── 000-similar-in-spirit-test.oh │ ├── 010-intro-manual.oh │ ├── 100-commands-manual.oh │ ├── 120-simple-manual.oh │ ├── 130-redirection-manual.oh │ ├── 140-pipelines-manual.oh │ ├── 150-globs-manual.oh │ ├── 160-quoting-manual.oh │ ├── 200-programming-manual.oh │ ├── 240-control-manual.oh │ ├── 243-control-while-manual.oh │ ├── 252-objects-object-manual.oh │ ├── 253-objects-method-manual.oh │ ├── 254-objects-point-manual.oh │ ├── 255-objects-patterns-manual.oh │ ├── 256-objects-syntax-manual.oh │ ├── 260-maps-manual.oh │ └── 280-channels-manual.oh ├── test.oh └── type-common.oh ├── doc ├── comparison.html └── manual.md ├── go.mod ├── go.sum ├── internal ├── common │ ├── common.go │ ├── interface │ │ ├── boolean │ │ │ └── boolean.go │ │ ├── cell │ │ │ └── cell.go │ │ ├── conduit │ │ │ └── conduit.go │ │ ├── integer │ │ │ └── integer.go │ │ ├── literal │ │ │ └── literal.go │ │ ├── rational │ │ │ └── rational.go │ │ ├── reference │ │ │ └── reference.go │ │ └── scope │ │ │ └── scope.go │ ├── struct │ │ ├── frame │ │ │ └── frame.go │ │ ├── hash │ │ │ └── hash.go │ │ ├── loc │ │ │ └── loc.go │ │ ├── slot │ │ │ └── slot.go │ │ └── token │ │ │ └── token.go │ ├── type │ │ ├── chn │ │ │ ├── chn.go │ │ │ ├── chn_internal_test.go │ │ │ └── generated.go │ │ ├── create │ │ │ └── create.go │ │ ├── env │ │ │ ├── env.go │ │ │ └── generated.go │ │ ├── list │ │ │ └── list.go │ │ ├── num │ │ │ ├── generated.go │ │ │ └── num.go │ │ ├── obj │ │ │ ├── generated.go │ │ │ └── obj.go │ │ ├── pair │ │ │ ├── generated.go │ │ │ └── pair.go │ │ ├── pipe │ │ │ ├── generated.go │ │ │ ├── pipe.go │ │ │ └── pipe_internal_test.go │ │ ├── status │ │ │ ├── generated.go │ │ │ └── status.go │ │ ├── str │ │ │ ├── generated.go │ │ │ └── str.go │ │ └── sym │ │ │ ├── conversion.go │ │ │ ├── plus.go │ │ │ └── sym.go │ └── validate │ │ └── validate.go ├── engine │ ├── boot │ │ ├── boot.go │ │ └── boot.oh │ ├── commands │ │ ├── arithmetic.go │ │ ├── channel.go │ │ ├── commands.go │ │ ├── conduit.go │ │ ├── core.go │ │ ├── file.go │ │ ├── list.go │ │ ├── logical.go │ │ ├── number.go │ │ ├── pair.go │ │ ├── pipe.go │ │ ├── relational.go │ │ ├── string.go │ │ ├── symbol.go │ │ └── umask_unix.go │ ├── engine.go │ └── task │ │ ├── action.go │ │ ├── closure.go │ │ ├── method.go │ │ ├── operation.go │ │ ├── registers.go │ │ ├── state.go │ │ ├── syntax.go │ │ └── task.go ├── reader │ ├── lexer │ │ ├── lexer.go │ │ └── lexer_internal_test.go │ ├── parser │ │ ├── parser.go │ │ └── parser_internal_test.go │ └── reader.go └── system │ ├── cache │ └── cache.go │ ├── history │ ├── history.go │ └── os_unix.go │ ├── job │ └── job_unix.go │ ├── options │ └── options.go │ └── process │ └── process_unix.go └── main.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: michaelmacinnis 2 | patreon: "user?u=20026503" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | local.* 3 | oh* 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - exhaustivestruct 5 | - paralleltest 6 | - scopelint 7 | - wrapcheck 8 | 9 | linters-settings: 10 | exhaustive: 11 | default-signifies-exhaustive: true 12 | funlen: 13 | lines: 96 14 | statements: 64 15 | 16 | issues: 17 | exclude-rules: 18 | - path: _test\.go 19 | linters: 20 | - errcheck 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | michaelmacinnis 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you would like to contribute to oh, please discuss the change first via the 2 | issue tracker. 3 | 4 | When contributing, you must agree to license your code under the same license 5 | as the existing source sode for oh. See [LICENSE](LICENSE) for details. 6 | 7 | On your first pull request, add your GitHub username to [AUTHORS](AUTHORS) to 8 | affirm that the code you are contributing is your own and that you agree to 9 | these terms. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011-2022 Michael MacInnis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oh, a new Unix shell 2 | 3 | ## Why oh? 4 | 5 | Oh is a reimagining of the Unix shell. 6 | 7 | Oh provides: 8 | 9 | - A simplified set of evaluation and quoting rules; 10 | - Rich return values that work with standard shell constructs; 11 | - First-class channels, pipes, environments and functions; 12 | - A list type (no word splitting); 13 | - Support for modularity; 14 | - Lexical scope; 15 | - Exceptions; 16 | - Kernel-style fexprs (allowing the definition of new language constructs); and 17 | - A syntax that deviates as little as possible from established conventions; 18 | 19 | Oh was motivated by the belief that many of the flaws in current Unix shells 20 | are not inherent but rather historical. Design choices that are now clearly 21 | unfortunate in retrospect have been carried forward in the name of backward 22 | compatibility. 23 | 24 | Oh's goal is a language that is not only more powerful and more regular but 25 | one that respects the conventions established by the Unix shell over the last 26 | half-century. 27 | 28 | ## Getting started 29 | 30 | ### Installing 31 | 32 | The easiest way to try oh is to download a precompiled binary. 33 | 34 | 35 | #### DragonFly BSD 36 | 37 | [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-dragonfly-amd64) 38 | 39 | #### FreeBSD 40 | 41 | [386](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-freebsd-386), [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-freebsd-amd64), [arm](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-freebsd-arm), [arm64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-freebsd-arm64), [riscv64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-freebsd-riscv64) 42 | 43 | #### illumos 44 | 45 | [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-illumos-amd64) 46 | 47 | #### Linux 48 | 49 | [386](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-386), [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-amd64), [arm](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-arm), [arm64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-arm64), [mips](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-mips), [mips64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-mips64), [mips64le](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-mips64le), [mipsle](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-mipsle), [ppc64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-ppc64), [ppc64le](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-ppc64le), [riscv64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-riscv64), [s390x](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-linux-s390x) 50 | 51 | #### macOS 52 | 53 | [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-darwin-amd64), [arm64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-darwin-arm64) 54 | 55 | #### OpenBSD 56 | 57 | [386](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-openbsd-386), [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-openbsd-amd64), [arm](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-openbsd-arm), [arm64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-openbsd-arm64) 58 | 59 | #### Solaris 60 | 61 | [amd64](https://github.com/michaelmacinnis/oh/releases/download/v0.8.3/oh-v0.8.3-solaris-amd64) 62 | 63 | You can also build oh from source. With Go 1.21 or later installed, type, 64 | 65 | go install github.com/michaelmacinnis/oh@v0.8.3 66 | 67 | ### Configuring 68 | 69 | When oh starts, it attempts to read a file called `.oh-rc` in the home 70 | directory of the current user. You can override this path by setting 71 | the OH_RC environment variable to the full path of an alternative file 72 | before invoking oh. 73 | 74 | The oh rc file is useful for setting environment variables and defining 75 | custom commands. It's also a good place to override oh's default prompt. 76 | The command below replaces oh's default prompt method with one that 77 | displays the current date. 78 | 79 | replace-make-prompt (method (suffix) { 80 | return `(date)$suffix 81 | }) 82 | 83 | Oh (thanks to peterh/liner) also provides a searchable command history. 84 | By default, this history is stored in a file called `.oh-history` in 85 | your home directory. You can override this by setting the OH_HISTORY 86 | environment variable to the full path of an alternative file before 87 | invoking oh. 88 | 89 | ## Comparing oh to other Unix shells 90 | 91 | Oh is a Unix shell. If you've used other Unix shells, oh should feel 92 | familiar. Below are some specific differences you may encounter. 93 | 94 | ### Clobbering 95 | 96 | When redirecting output oh will not overwrite an existing file. To force 97 | oh to overwrite (clobber) an existing file add a pipe, `|`, character 98 | immediately after the redirection operator. For example, 99 | 100 | command >| out.txt 101 | 102 | Oh's pipe and redirection syntax is as follows. 103 | 104 | | Syntax | Redirection | 105 | |----------:|:----------------------------------:| 106 | | `<` | input-from | 107 | | `>` | output-to | 108 | | `>&` | output-errors-to | 109 | | `>&\|` | output-errors-clobbers | 110 | | `>>` | append-output-to | 111 | | `>>&` | append-output-errors-to | 112 | | `>\|` | output-clobbers | 113 | | `\|` | pipe-output-to | 114 | | `\|&` | pipe-output-errors-to | 115 | | `\|<` | -named-pipe-input-from* | 116 | | `\|>` | -named-pipe-output-to* | 117 | 118 | \* - Used in process substitution. 119 | 120 | ### Command substitution 121 | 122 | Many Unix shells support command substitution using the historical 123 | backtick syntax, 124 | 125 | `command` 126 | 127 | or the POSIX syntax, 128 | 129 | $(command) 130 | 131 | Oh has one syntax for command substitution, 132 | 133 | `(command) 134 | 135 | This syntax is both nestable and unambiguous. 136 | 137 | ### Here documents 138 | 139 | Oh does not have here documents. It does however allow strings to span 140 | lines and provides a `here` command that takes a string argument and can 141 | be used to the same effect. For example, 142 | 143 | # Build oh for supported BSD platforms 144 | here " 145 | dragonfly amd64 146 | freebsd 386 147 | freebsd amd64 148 | freebsd arm 149 | freebsd arm64 150 | openbsd 386 151 | openbsd amd64 152 | openbsd arm 153 | openbsd arm64 154 | openbsd mips64 155 | " | mill (o a) { 156 | echo ${o}/${a} 157 | GOOS=${o} GOARCH=${a} go build -o oh-latest-${o}-${a} 158 | } 159 | 160 | ### Variables 161 | 162 | To introduce a new variable, use the `define` command, 163 | 164 | define x 3 165 | 166 | To introduce a variable that will be visible to external processes, 167 | use the `export` command, 168 | 169 | export GOROOT /usr/local/go 170 | 171 | To set the value of an existing variable, use the `set` command, 172 | 173 | set x 4 174 | 175 | ### Variables and implicit concatenation 176 | 177 | Like other shells, oh implicitly concatenates adjacent string/symbol 178 | values. Unlike other shells, oh allows a larger set of characters to 179 | appear in variable names. In addition to letters, numbers, and the 180 | underscore character, the following characters, 181 | 182 | '!', '%', '*', '+', '-', '?', '[', ']', and '^' 183 | 184 | can be used in oh variable names. The command, 185 | 186 | echo $set! 187 | 188 | will cause oh to attempt to resolve a variable called `set!`. 189 | The following characters, 190 | 191 | ',', '.', '/', ':', '=', '@', and '~' 192 | 193 | always result in a symbol of one character. This ensures that commands 194 | like, 195 | 196 | cd $PWD/$dir 197 | 198 | work as expected. When using implicit concatentation, unexpected behavior 199 | can be avoided by enclosing variable names in braces. 200 | 201 | ### More detailed comparison 202 | 203 | For a detailed comparison to other Unix shells see: [Comparing oh to other Unix Shells](https://htmlpreview.github.io/?https://raw.githubusercontent.com/michaelmacinnis/oh/master/doc/comparison.html) 204 | 205 | ## Using oh 206 | 207 | For more information on using oh, see: [Using oh](doc/manual.md) 208 | 209 | ## Contributing to oh 210 | 211 | Oh is an ongoing experiment and it needs your help. Try oh. Let me know 212 | what works for you and what doesn't. 213 | 214 | Pull requests are welcome. For information on contributing, see: [CONTRIBUTING](CONTRIBUTING.md) 215 | 216 | You can also sponsor me through GitHub Sponsors or Patreon. 217 | 218 | ## License 219 | 220 | [MIT](LICENSE) 221 | 222 | -------------------------------------------------------------------------------- /bin/build.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | # To see missing GOOS/GOARCH pairs compare with: go tool dist list 4 | 5 | define t `(git describe --abbrev=0 --tags) 6 | 7 | here " 8 | aix ppc64 - cannot use unix.TIOCSPGRP (untyped int constant 18446744071562359926) as int value in argument to unix.IoctlSetPointerInt (overflows) 9 | darwin amd64 10 | darwin arm64 11 | dragonfly amd64 12 | freebsd 386 13 | freebsd amd64 14 | freebsd arm 15 | freebsd arm64 16 | freebsd riscv64 17 | illumos amd64 18 | linux 386 19 | linux amd64 20 | linux arm 21 | linux arm64 22 | linux mips 23 | linux mips64 24 | linux mips64le 25 | linux mipsle 26 | linux ppc64 27 | linux ppc64le 28 | linux riscv64 29 | linux s390x 30 | netbsd 386 - undefined: unix.WCONTINUED 31 | netbsd amd64 - undefined: unix.WCONTINUED 32 | netbsd arm - undefined: unix.WCONTINUED 33 | netbsd arm64 - undefined: unix.WCONTINUED 34 | openbsd 386 35 | openbsd amd64 36 | openbsd arm 37 | openbsd arm64 38 | solaris amd64 39 | " | grep -v ' - ' | sed -re 's/ - .*$//g' | mill (o a) { 40 | echo ${o}/${a} 41 | GOOS=${o} GOARCH=${a} go build -ldflags='-s -w' -o oh-${t}-${o}-${a} -trimpath 42 | } 43 | -------------------------------------------------------------------------------- /bin/check.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | golangci-lint run --sort-results | grep -Fv TODO 4 | 5 | # To see the silenced linter warnings. 6 | # git grep nolint | grep -Ev 'checkno(globals|inits)' | grep -v implements 7 | -------------------------------------------------------------------------------- /bin/doc.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | if (ne? 2 (@ length)) { 4 | error "usage: $0 " 5 | exit 1 6 | } 7 | 8 | define dir: ... $ORIGIN doctest 9 | define pattern $1 10 | define output $2 11 | 12 | define base `(basename $output) 13 | echo "generating ${base}" 14 | 15 | export stdout: open 'w' (mend '/' $ORIGIN $output) 16 | 17 | find $dir -name "[0-9]*.oh" | 18 | grep -i $pattern | sort | 19 | while (define path: read-line) { 20 | awk ' 21 | BEGIN { code = 0 } 22 | $0 ~ /^#[#+]/ { print substr($0, 4) } 23 | $1 ~ /^#[}]/ { code = 0 } 24 | code > 0 { print " " $0 } 25 | $1 ~ /^#[{]/ { code = 1 } 26 | ' $path 27 | } 28 | -------------------------------------------------------------------------------- /bin/doctest/000-continuations-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## Oh is properly tail-recursive and exposes continuations as first-class 4 | ## values: 5 | ## 6 | #{ 7 | define label: method () { 8 | return $return 9 | } 10 | 11 | define continue: method (label) { 12 | label $label 13 | } 14 | 15 | define count: number 0 16 | 17 | define loop: label 18 | 19 | if (lt? $count 100) { 20 | set count: add $count 1 21 | echo "Hello, World! (${count})" 22 | continue $loop 23 | } 24 | #} 25 | ## 26 | 27 | #- Hello, World! (1) 28 | #- Hello, World! (2) 29 | #- Hello, World! (3) 30 | #- Hello, World! (4) 31 | #- Hello, World! (5) 32 | #- Hello, World! (6) 33 | #- Hello, World! (7) 34 | #- Hello, World! (8) 35 | #- Hello, World! (9) 36 | #- Hello, World! (10) 37 | #- Hello, World! (11) 38 | #- Hello, World! (12) 39 | #- Hello, World! (13) 40 | #- Hello, World! (14) 41 | #- Hello, World! (15) 42 | #- Hello, World! (16) 43 | #- Hello, World! (17) 44 | #- Hello, World! (18) 45 | #- Hello, World! (19) 46 | #- Hello, World! (20) 47 | #- Hello, World! (21) 48 | #- Hello, World! (22) 49 | #- Hello, World! (23) 50 | #- Hello, World! (24) 51 | #- Hello, World! (25) 52 | #- Hello, World! (26) 53 | #- Hello, World! (27) 54 | #- Hello, World! (28) 55 | #- Hello, World! (29) 56 | #- Hello, World! (30) 57 | #- Hello, World! (31) 58 | #- Hello, World! (32) 59 | #- Hello, World! (33) 60 | #- Hello, World! (34) 61 | #- Hello, World! (35) 62 | #- Hello, World! (36) 63 | #- Hello, World! (37) 64 | #- Hello, World! (38) 65 | #- Hello, World! (39) 66 | #- Hello, World! (40) 67 | #- Hello, World! (41) 68 | #- Hello, World! (42) 69 | #- Hello, World! (43) 70 | #- Hello, World! (44) 71 | #- Hello, World! (45) 72 | #- Hello, World! (46) 73 | #- Hello, World! (47) 74 | #- Hello, World! (48) 75 | #- Hello, World! (49) 76 | #- Hello, World! (50) 77 | #- Hello, World! (51) 78 | #- Hello, World! (52) 79 | #- Hello, World! (53) 80 | #- Hello, World! (54) 81 | #- Hello, World! (55) 82 | #- Hello, World! (56) 83 | #- Hello, World! (57) 84 | #- Hello, World! (58) 85 | #- Hello, World! (59) 86 | #- Hello, World! (60) 87 | #- Hello, World! (61) 88 | #- Hello, World! (62) 89 | #- Hello, World! (63) 90 | #- Hello, World! (64) 91 | #- Hello, World! (65) 92 | #- Hello, World! (66) 93 | #- Hello, World! (67) 94 | #- Hello, World! (68) 95 | #- Hello, World! (69) 96 | #- Hello, World! (70) 97 | #- Hello, World! (71) 98 | #- Hello, World! (72) 99 | #- Hello, World! (73) 100 | #- Hello, World! (74) 101 | #- Hello, World! (75) 102 | #- Hello, World! (76) 103 | #- Hello, World! (77) 104 | #- Hello, World! (78) 105 | #- Hello, World! (79) 106 | #- Hello, World! (80) 107 | #- Hello, World! (81) 108 | #- Hello, World! (82) 109 | #- Hello, World! (83) 110 | #- Hello, World! (84) 111 | #- Hello, World! (85) 112 | #- Hello, World! (86) 113 | #- Hello, World! (87) 114 | #- Hello, World! (88) 115 | #- Hello, World! (89) 116 | #- Hello, World! (90) 117 | #- Hello, World! (91) 118 | #- Hello, World! (92) 119 | #- Hello, World! (93) 120 | #- Hello, World! (94) 121 | #- Hello, World! (95) 122 | #- Hello, World! (96) 123 | #- Hello, World! (97) 124 | #- Hello, World! (98) 125 | #- Hello, World! (99) 126 | #- Hello, World! (100) 127 | 128 | -------------------------------------------------------------------------------- /bin/doctest/000-dynamic-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | define a "lexical" 4 | 5 | define foo: method () { 6 | echo $a $caller 7 | } 8 | 9 | define bar: method () { 10 | define a "local" 11 | export caller "bar" 12 | 13 | foo 14 | } 15 | 16 | bar 17 | 18 | define baz: method () { 19 | export a "nice try" 20 | export caller "baz" 21 | 22 | foo 23 | } 24 | 25 | baz 26 | 27 | #- lexical bar 28 | #- lexical baz 29 | 30 | -------------------------------------------------------------------------------- /bin/doctest/000-exceptions-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | export y "and" 4 | define m1: method () { 5 | export x "Hello" 6 | define z "Goodbye" 7 | m2 8 | m2 9 | } 10 | define m2: method () { 11 | catch ignored { 12 | echo Here 13 | return 14 | } 15 | 16 | echo $x 17 | echo $y 18 | 19 | set y "then" 20 | 21 | echo $z 22 | 23 | } 24 | m1 25 | m2 26 | 27 | #- Hello 28 | #- and 29 | #- Here 30 | #- Hello 31 | #- then 32 | #- Here 33 | #- Here 34 | 35 | -------------------------------------------------------------------------------- /bin/doctest/000-homoiconic-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## Oh uses the same syntax for code and data. This enables it to be easily 4 | ## extended: 5 | ## 6 | #{ 7 | # The short-circuit and operator is defined using the syntax command. 8 | define and: syntax ((lst)) e { 9 | define r () 10 | while $lst { 11 | set r (e eval (lst head)) 12 | if (not $r) { 13 | return $r 14 | } 15 | set lst (lst tail) 16 | } 17 | return $r 18 | } 19 | echo (and true () (echo "Never reached")) 20 | #} 21 | ## 22 | 23 | #- () 24 | -------------------------------------------------------------------------------- /bin/doctest/000-local-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | define blah: method () { 4 | define foo = "I'm a local variable!" 5 | } 6 | 7 | blah 8 | 9 | echo $foo 10 | 11 | #- 9:1: echo $foo 12 | #- error: 'foo' not defined 13 | -------------------------------------------------------------------------------- /bin/doctest/000-similar-in-spirit-test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | echo "Hello, World!" 4 | cal 01 2030 | sed -re 's/[ ]+$//g' # Strip trailing spaces. 5 | cal 01 2030 | sed -re 's/[ ]+$//g' >greeting 6 | echo "Hello, World!" >>greeting 7 | wc file 14 | #} 15 | ## 16 | ## The notation `>file` is interpreted by the shell and is not passed as an 17 | ## argument to `ls`. If the file does not exist then the shell creates it. 18 | ## If the file already exists, oh will refuse to clobber the file. 19 | ## Output may also be appended to a file. 20 | ## 21 | #{ 22 | ls >> file 23 | #} 24 | ## 25 | ## Standard output and standard error may be redirected, 26 | ## 27 | #{ 28 | ls non-existent-filename >&errors 29 | #} 30 | ## 31 | ## or appended to a file. 32 | ## 33 | #{ 34 | ls errors >>&errors 35 | #} 36 | ## 37 | ## Standard input may also be redirected. 38 | ## 39 | ## wc -l | file 47 | ## 48 | 49 | sort file | awk '{ print "stdout" FS count++ FS $0 }' 50 | sort errors | awk '{ print "stdout and stderr" FS count++ FS $0 }' 51 | rm errors file 1 2 3 52 | cd - 53 | rmdir /tmp/redirection 54 | 55 | #- 8 56 | #- 2 57 | #- stdout 0 1 58 | #- stdout 1 1 59 | #- stdout 2 2 60 | #- stdout 3 2 61 | #- stdout 4 3 62 | #- stdout 5 3 63 | #- stdout 6 file 64 | #- stdout 7 file 65 | #- stdout and stderr 0 errors 66 | #- stdout and stderr 1 ls: cannot access 'non-existent-filename': No such file or directory 67 | 68 | -------------------------------------------------------------------------------- /bin/doctest/140-pipelines-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | mkdir /tmp/pipelines 4 | cd /tmp/pipelines 5 | touch 1 2 3 6 | 7 | ## ### Pipelines and Filters 8 | ## 9 | ## The standard output of one command may be connected to the standard input 10 | ## of another command using the pipe operator. 11 | ## 12 | ## ls | wc -l 13 | ls | wc -l | tr -s ' ' | sed -e 's/^[ ]*//g' # Remove duplicate spaces and leading whitespace. 14 | ## 15 | ## The commands connected in this way constitute a pipeline. The overall 16 | ## effect is the same as, 17 | ## 18 | ## ls >file; wc -l file 19 | ls >file; wc -l file | tr -s ' ' | sed -e 's/^[ ]*//g' # Remove duplicate spaces and leading whitespace. 20 | ## 21 | ## except that no file is used. Instead the two commands are connected by a 22 | ## pipe and are run in parallel. 23 | ## 24 | ## A pipeline may consist of more than two commands. 25 | ## 26 | ## ls | grep old | wc -l 27 | ls | grep old | wc -l | tr -s ' ' | sed -e 's/^[ ]*//g' # Remove duplicate spaces and leading whitespace. 28 | ## 29 | 30 | #- 3 31 | #- 4 file 32 | #- 0 33 | 34 | rm file 1 2 3 35 | cd - 36 | rmdir /tmp/pipelines 37 | 38 | -------------------------------------------------------------------------------- /bin/doctest/150-globs-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | mkdir /tmp/globs 4 | cd /tmp/globs 5 | touch a.1 b.2 3.go 4 .hidden 6 | 7 | ## ### File Name Generation 8 | ## 9 | ## The oh shell provides a mechanism for generating a list of file names that 10 | ## match a pattern. The patterns are called globs. The glob, `*.go` in the 11 | ## command, 12 | ## 13 | #{ 14 | ls *.go 15 | #} 16 | ## 17 | ## generates, as arguments to `ls`, all file names in the current directory 18 | ## that end in `.go`. The character * is a pattern that will match any string 19 | ## including the empty string. In general patterns are specified as follows. 20 | ## 21 | ## | Pattern | Action | 22 | ## |:-------:|:---------------------------------------------------------------| 23 | ## | `*` | Matches any sequence of zero or more characters. | 24 | ## | `?` | Matches any single character. | 25 | ## | `[...]` | Matches any one of the characters enclosed. A pair separated by a hyphen, `-`, will match a lexical range of characters. If the first enclosed character is a `^` the match is negated. | 26 | ## 27 | ## For example, 28 | ## 29 | #{ 30 | ls [a-z]* 31 | #} 32 | ## 33 | ## matches all names in the current directory beginning with on of the letters 34 | ## `a` through `z`, while, 35 | ## 36 | #{ 37 | ls ? 38 | #} 39 | ## 40 | ## matches all names in the current directory that consist of a single 41 | ## character. 42 | ## 43 | ## There is one exception to the general rules given for patterns. The 44 | ## character `.` at the start of a file name must be explicitly matched. 45 | ## 46 | #{ 47 | echo * 48 | #} 49 | ## 50 | ## will therefore echo all file names not beginning with a `.` in the current 51 | ## directory, while, 52 | ## 53 | #{ 54 | echo .* 55 | #} 56 | ## 57 | ## will echo all those file names that begin with `.` as the `.` was explicitly 58 | ## specified. This avoids inadvertent matching of the names `.` and `..` which 59 | ## mean the current directory and the parent directory, respectively. 60 | ## 61 | 62 | #- 3.go 63 | #- a.1 64 | #- b.2 65 | #- 4 66 | #- 3.go 4 a.1 b.2 67 | #- .hidden 68 | 69 | rm a.1 b.2 3.go 4 .hidden 70 | cd - 71 | rmdir /tmp/globs 72 | 73 | -------------------------------------------------------------------------------- /bin/doctest/160-quoting-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## ### Quoting 4 | ## 5 | ## Characters that have a special meaning to the shell, such as `<` and `>`, 6 | ## are called metacharacters. These characters must be quoted to strip them of 7 | ## their special meaning. 8 | ## 9 | #{ 10 | echo '?' 11 | #} 12 | ## 13 | ## will echo a single `?', 14 | ## 15 | 16 | #- ? 17 | 18 | ## while, 19 | ## 20 | #{ 21 | echo "xx**\"**xx" 22 | #} 23 | ## 24 | ## will echo, 25 | ## 26 | #+ xx**"**xx 27 | ## 28 | ## A double quoted string may not contain an unescaped double quote but may 29 | ## contain newlines, which are preserved, and escape sequences which are 30 | ## interpreted. Escape sequences are not interpreted in a single quoted 31 | ## string. A single quoted string may not contain a single quote as there is 32 | ## no way to escape it. 33 | ## 34 | #{ 35 | echo "Hello, 36 | World!" 37 | #} 38 | ## 39 | ## Double quoted strings also automatically perform string interpolation. 40 | ## In a double quoted string, a dollar sign, `$`, followed by a variable name, 41 | ## optionally enclosed in braces, will be replaced by the variable's value. 42 | ## If no variable exists an exception is thrown. While the opening and closing 43 | ## braces are not required their use is encouraged to avoid ambiguity. 44 | ## 45 | 46 | #- Hello, 47 | #- World! 48 | 49 | define x 'Hello, World!' 50 | echo "${x}" 51 | 52 | #- Hello, World! 53 | 54 | -------------------------------------------------------------------------------- /bin/doctest/200-programming-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## ## Using oh Programmatically 4 | ## 5 | ## In addition to providing a command-line interface to Unix and Unix-like 6 | ## systems, oh is also a programming language. 7 | ## 8 | 9 | -------------------------------------------------------------------------------- /bin/doctest/240-control-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## ### Control Structures 4 | ## 5 | 6 | -------------------------------------------------------------------------------- /bin/doctest/243-control-while-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## #### While 4 | ## 5 | ## Oh has a fairly standard pre-test loop. The commands, 6 | ## 7 | #{ 8 | define x 0 9 | while (lt? $x 10) { 10 | echo $x 11 | set x: add $x 1 12 | } 13 | #} 14 | ## 15 | ## produce the output, 16 | ## 17 | #+ 0 18 | #+ 1 19 | #+ 2 20 | #+ 3 21 | #+ 4 22 | #+ 5 23 | #+ 6 24 | #+ 7 25 | #+ 8 26 | #+ 9 27 | ## 28 | ## 29 | 30 | -------------------------------------------------------------------------------- /bin/doctest/252-objects-object-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## #### Object 4 | ## 5 | ## In oh, environments are first-class values with public and private halves. 6 | ## For a variable to be public it must be created with the `export` command 7 | ## instead of the `define` command. A reference to an environment can be 8 | ## created with the `object` command. 9 | ## 10 | #{ 11 | define o: object { 12 | export get $resolve 13 | 14 | export x 1 15 | define y 2 16 | } 17 | 18 | echo "public member" (o get x) 19 | echo "private member" (o get y) 20 | #} 21 | ## 22 | 23 | #- public member 1 24 | #- 19:24: o get y) 25 | #- error: 'y' not defined 26 | -------------------------------------------------------------------------------- /bin/doctest/253-objects-method-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## #### Method 4 | ## 5 | ## A sequence of actions can be saved with the `method` command. 6 | ## 7 | #{ 8 | define hello: method () { 9 | echo "Hello, World!" 10 | } 11 | #} 12 | ## 13 | ## Once defined, a method can be called in the same way as other commands. 14 | ## 15 | #{ 16 | hello 17 | #} 18 | ## 19 | 20 | #- Hello, World! 21 | 22 | ## Methods can have named parameters. 23 | ## 24 | #{ 25 | define sum3: method (a b c) { 26 | add $a $b $c 27 | } 28 | echo (sum3 1 2 3) 29 | #} 30 | ## 31 | 32 | #- 6 33 | 34 | ## Methods may have a self parameter. The name for the self parameter must 35 | ## appear before the list of arguments. 36 | ## 37 | -------------------------------------------------------------------------------- /bin/doctest/254-objects-point-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | #{ 4 | define point: method (r s) = (object { 5 | define x: add 0 $r 6 | define y: add 0 $s 7 | 8 | export get-x: method () { 9 | return x 10 | } 11 | 12 | export get-y: method () { 13 | return y 14 | } 15 | 16 | export move: method self (a b) { 17 | set x: add $x $a 18 | set y: add $y $b 19 | } 20 | 21 | export show: method () { 22 | echo $x $y 23 | } 24 | }) 25 | 26 | define p: point 0 0 27 | p move 1 2 28 | p show 29 | #} 30 | ## 31 | 32 | #- 1 2 33 | 34 | -------------------------------------------------------------------------------- /bin/doctest/255-objects-patterns-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## Shared behavior can be implemented by defining a method in an outer scope 4 | ## and explicitly pulling that method "up". 5 | ## 6 | ## The following code, 7 | ## 8 | #{ 9 | export me: method self () { 10 | echo 'my name is:' (self name) 11 | } 12 | 13 | define x: object { 14 | export me $me 15 | export name: method () { 16 | return 'x' 17 | } 18 | } 19 | 20 | x me 21 | #} 22 | ## 23 | ## produces the output, 24 | ## 25 | #+ my name is: x 26 | ## 27 | 28 | ## An object may redirect a call to another object. The code below, 29 | ## 30 | #{ 31 | define z: object { 32 | export me $me 33 | export name: method () { 34 | return 'z' 35 | } 36 | export you: method () { 37 | x me # Redirection. 38 | } 39 | } 40 | 41 | z me 42 | z you 43 | #} 44 | ## 45 | ## produces the output, 46 | ## 47 | #+ my name is: z 48 | #+ my name is: x 49 | ## 50 | 51 | -------------------------------------------------------------------------------- /bin/doctest/256-objects-syntax-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## #### Syntax 4 | ## 5 | ## Oh can be extended with the `syntax` command. The `syntax` command is 6 | ## very similar to the `method` command except that the methods it creates 7 | ## are passed their arguments unevaluated. The `eval` command can be used 8 | ## to explicitly evaluate arguments. A name may be specified for the calling 9 | ## environment after the list of arguments. This can then be used to 10 | ## evaluate arguments in the calling environment. 11 | ## 12 | ## The example below uses the `syntax` command to define a new `until` command. 13 | ## 14 | #{ 15 | define until: syntax (condition (body)) e { 16 | e eval (cons while (cons (list not $condition) $body)) 17 | } 18 | 19 | define x 0 20 | until (eq? 10 $x) { 21 | echo $x 22 | set x: add $x 1 23 | } 24 | #} 25 | ## 26 | 27 | #- 0 28 | #- 1 29 | #- 2 30 | #- 3 31 | #- 4 32 | #- 5 33 | #- 6 34 | #- 7 35 | #- 8 36 | #- 9 37 | -------------------------------------------------------------------------------- /bin/doctest/260-maps-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## ### Maps 4 | ## 5 | ## Using oh's map type, it is relatively simple to record the exit status 6 | ## for each stage in a pipeline. The code below, 7 | ## 8 | #{ 9 | define exit-status: map 10 | 11 | define pipe-fitting: method (label (cmd)) e { 12 | exit-status set $label (e eval $cmd) 13 | } 14 | 15 | pipe-fitting 1st echo 1 2 3 | 16 | pipe-fitting 2nd tr ' ' '\n' | 17 | pipe-fitting 3rd grep 2 | 18 | pipe-fitting 4th grep 3 19 | 20 | echo '1st stage exit status =>' (exit-status get 1st) 21 | echo '2nd stage exit status =>' (exit-status get 2nd) 22 | echo '3rd stage exit status =>' (exit-status get 3rd) 23 | echo '4th stage exit status =>' (exit-status get 4th) 24 | #} 25 | ## 26 | ## produces the output, 27 | ## 28 | #+ 1st stage exit status => 0 29 | #+ 2nd stage exit status => 0 30 | #+ 3rd stage exit status => 0 31 | #+ 4th stage exit status => 1 32 | ## 33 | 34 | -------------------------------------------------------------------------------- /bin/doctest/280-channels-manual.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | ## ### Channels 4 | ## 5 | ## Oh exposes channels as first-class values. Channels allow particularly 6 | ## elegant solutions to some problems, as shown in the prime sieve example 7 | ## below (adapted from "Newsqueak: A Language for Communicating with Mice"). 8 | ## 9 | #{ 10 | 11 | define filter: method (base) { 12 | mill (n) { 13 | mod $n $base && write $n 14 | } 15 | } 16 | 17 | define connector: chan 18 | 19 | spawn { 20 | define n: number 1 21 | while true { 22 | write (set n: add $n 1) 23 | } 24 | } >$connector 25 | 26 | define prime-numbers: chan 27 | 28 | while true { 29 | define prime: connector read 30 | write $prime 31 | 32 | define filtered: chan 33 | spawn { 34 | filter $prime 35 | } <$connector >$filtered 36 | 37 | set connector $filtered 38 | } >$prime-numbers & 39 | 40 | 41 | define count: number 100 42 | printf "The first %d prime numbers\n" $count 43 | 44 | define line '' 45 | while $count { 46 | define p: prime-numbers read 47 | 48 | set line: mend '' $line (str format "%7.7s" $p) 49 | 50 | set count: sub $count 1 51 | mod $count 10 || block { 52 | echo $line 53 | set line '' 54 | } 55 | } 56 | #} 57 | ## 58 | 59 | #- The first 100 prime numbers 60 | #- 2 3 5 7 11 13 17 19 23 29 61 | #- 31 37 41 43 47 53 59 61 67 71 62 | #- 73 79 83 89 97 101 103 107 109 113 63 | #- 127 131 137 139 149 151 157 163 167 173 64 | #- 179 181 191 193 197 199 211 223 227 229 65 | #- 233 239 241 251 257 263 269 271 277 281 66 | #- 283 293 307 311 313 317 331 337 347 349 67 | #- 353 359 367 373 379 383 389 397 401 409 68 | #- 419 421 431 433 439 443 449 457 461 463 69 | #- 467 479 487 491 499 503 509 521 523 541 70 | 71 | -------------------------------------------------------------------------------- /bin/test.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | define dir: ... $ORIGIN doctest 4 | define oh: ... $ORIGIN oh 5 | 6 | define prefix-lines: method (prefix) { 7 | define count: number 1 8 | while (define line: read-line) { 9 | echo (mend ': ' $prefix $count $line) 10 | set count: add $count 1 11 | } 12 | } 13 | 14 | # Run tests. 15 | find $dir -name "[0-9]*.oh" | grep -Fv unused | sort | 16 | while (define path: read-line) { 17 | define file `(basename $path) 18 | echo running $file 19 | diff |<(grep "^#[+-] " $path | sed -e "s/^#[+-] //g" | prefix-lines $file) \ 20 | |<($oh $path |& prefix-lines $file | sed -Ee "s|${path}:||") 21 | } 22 | -------------------------------------------------------------------------------- /bin/type-common.oh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env oh 2 | 3 | export stdout: open 'w' (mend / $PWD $1 generated.go) 4 | 5 | echo '// Code generated by '`(basename $0)'. DO NOT EDIT.' 6 | echo 7 | echo '// Released under an MIT license. See LICENSE.' 8 | echo "package" `(basename $1) 9 | 10 | echo ' 11 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | 13 | // Is returns true if c is a *T. 14 | func Is(c cell.I) bool { 15 | _, ok := c.(*T) 16 | 17 | return ok 18 | } 19 | 20 | // To returns a *T if c is a *T; Otherwise it panics. 21 | func To(c cell.I) *T { 22 | if t, ok := c.(*T); ok { 23 | return t 24 | } 25 | 26 | panic("not a " + name) 27 | }' 28 | -------------------------------------------------------------------------------- /doc/manual.md: -------------------------------------------------------------------------------- 1 | # Using oh 2 | 3 | ## Using oh Interactively 4 | 5 | Oh provides a command-line interface to Unix and Unix-like systems. 6 | 7 | (Much of this section shamelessly copied from "An Introduction to the 8 | UNIX Shell") 9 | 10 | ### Simple Commands 11 | 12 | Simple commands consist of one or more words separated by blanks. The first 13 | word is the name of the command to be executed; any remaining words are 14 | passed as arguments to the command. For example, 15 | 16 | ls -l 17 | 18 | is a command that prints a list of files in the current directory. The 19 | argument `-l` tells `ls` to print status information, the size and the 20 | creation date for each file. 21 | 22 | Multiple commands may be written on the same line separated by a semicolon. 23 | 24 | ### Input/Output Redirection 25 | 26 | Standard input, standard output and standard error are initially connected 27 | to the terminal. Standard output may be sent to a file. 28 | 29 | ls > file 30 | 31 | The notation `>file` is interpreted by the shell and is not passed as an 32 | argument to `ls`. If the file does not exist then the shell creates it. 33 | If the file already exists, oh will refuse to clobber the file. 34 | Output may also be appended to a file. 35 | 36 | ls >> file 37 | 38 | Standard output and standard error may be redirected, 39 | 40 | ls non-existent-filename >&errors 41 | 42 | or appended to a file. 43 | 44 | ls errors >>&errors 45 | 46 | Standard input may also be redirected. 47 | 48 | wc -l | file 54 | 55 | ### Pipelines and Filters 56 | 57 | The standard output of one command may be connected to the standard input 58 | of another command using the pipe operator. 59 | 60 | ls | wc -l 61 | 62 | The commands connected in this way constitute a pipeline. The overall 63 | effect is the same as, 64 | 65 | ls >file; wc -l file 66 | 67 | except that no file is used. Instead the two commands are connected by a 68 | pipe and are run in parallel. 69 | 70 | A pipeline may consist of more than two commands. 71 | 72 | ls | grep old | wc -l 73 | 74 | ### File Name Generation 75 | 76 | The oh shell provides a mechanism for generating a list of file names that 77 | match a pattern. The patterns are called globs. The glob, `*.go` in the 78 | command, 79 | 80 | ls *.go 81 | 82 | generates, as arguments to `ls`, all file names in the current directory 83 | that end in `.go`. The character * is a pattern that will match any string 84 | including the empty string. In general patterns are specified as follows. 85 | 86 | | Pattern | Action | 87 | |:-------:|:---------------------------------------------------------------| 88 | | `*` | Matches any sequence of zero or more characters. | 89 | | `?` | Matches any single character. | 90 | | `[...]` | Matches any one of the characters enclosed. A pair separated by a hyphen, `-`, will match a lexical range of characters. If the first enclosed character is a `^` the match is negated. | 91 | 92 | For example, 93 | 94 | ls [a-z]* 95 | 96 | matches all names in the current directory beginning with on of the letters 97 | `a` through `z`, while, 98 | 99 | ls ? 100 | 101 | matches all names in the current directory that consist of a single 102 | character. 103 | 104 | There is one exception to the general rules given for patterns. The 105 | character `.` at the start of a file name must be explicitly matched. 106 | 107 | echo * 108 | 109 | will therefore echo all file names not beginning with a `.` in the current 110 | directory, while, 111 | 112 | echo .* 113 | 114 | will echo all those file names that begin with `.` as the `.` was explicitly 115 | specified. This avoids inadvertent matching of the names `.` and `..` which 116 | mean the current directory and the parent directory, respectively. 117 | 118 | ### Quoting 119 | 120 | Characters that have a special meaning to the shell, such as `<` and `>`, 121 | are called metacharacters. These characters must be quoted to strip them of 122 | their special meaning. 123 | 124 | echo '?' 125 | 126 | will echo a single `?', 127 | 128 | while, 129 | 130 | echo "xx**\"**xx" 131 | 132 | will echo, 133 | 134 | xx**"**xx 135 | 136 | A double quoted string may not contain an unescaped double quote but may 137 | contain newlines, which are preserved, and escape sequences which are 138 | interpreted. Escape sequences are not interpreted in a single quoted 139 | string. A single quoted string may not contain a single quote as there is 140 | no way to escape it. 141 | 142 | echo "Hello, 143 | World!" 144 | 145 | Double quoted strings also automatically perform string interpolation. 146 | In a double quoted string, a dollar sign, `$`, followed by a variable name, 147 | optionally enclosed in braces, will be replaced by the variable's value. 148 | If no variable exists an exception is thrown. While the opening and closing 149 | braces are not required their use is encouraged to avoid ambiguity. 150 | 151 | ## Using oh Programmatically 152 | 153 | In addition to providing a command-line interface to Unix and Unix-like 154 | systems, oh is also a programming language. 155 | 156 | ### Control Structures 157 | 158 | #### While 159 | 160 | Oh has a fairly standard pre-test loop. The commands, 161 | 162 | define x 0 163 | while (lt? $x 10) { 164 | echo $x 165 | set x: add $x 1 166 | } 167 | 168 | produce the output, 169 | 170 | 0 171 | 1 172 | 2 173 | 3 174 | 4 175 | 5 176 | 6 177 | 7 178 | 8 179 | 9 180 | 181 | 182 | #### Object 183 | 184 | In oh, environments are first-class values with public and private halves. 185 | For a variable to be public it must be created with the `export` command 186 | instead of the `define` command. A reference to an environment can be 187 | created with the `object` command. 188 | 189 | define o: object { 190 | export get $resolve 191 | 192 | export x 1 193 | define y 2 194 | } 195 | 196 | echo "public member" (o get x) 197 | echo "private member" (o get y) 198 | 199 | #### Method 200 | 201 | A sequence of actions can be saved with the `method` command. 202 | 203 | define hello: method () { 204 | echo "Hello, World!" 205 | } 206 | 207 | Once defined, a method can be called in the same way as other commands. 208 | 209 | hello 210 | 211 | Methods can have named parameters. 212 | 213 | define sum3: method (a b c) { 214 | add $a $b $c 215 | } 216 | echo (sum3 1 2 3) 217 | 218 | Methods may have a self parameter. The name for the self parameter must 219 | appear before the list of arguments. 220 | 221 | define point: method (r s) = (object { 222 | define x: add 0 $r 223 | define y: add 0 $s 224 | 225 | export get-x: method () { 226 | return x 227 | } 228 | 229 | export get-y: method () { 230 | return y 231 | } 232 | 233 | export move: method self (a b) { 234 | set x: add $x $a 235 | set y: add $y $b 236 | } 237 | 238 | export show: method () { 239 | echo $x $y 240 | } 241 | }) 242 | 243 | define p: point 0 0 244 | p move 1 2 245 | p show 246 | 247 | Shared behavior can be implemented by defining a method in an outer scope 248 | and explicitly pulling that method "up". 249 | 250 | The following code, 251 | 252 | export me: method self () { 253 | echo 'my name is:' (self name) 254 | } 255 | 256 | define x: object { 257 | export me $me 258 | export name: method () { 259 | return 'x' 260 | } 261 | } 262 | 263 | x me 264 | 265 | produces the output, 266 | 267 | my name is: x 268 | 269 | An object may redirect a call to another object. The code below, 270 | 271 | define z: object { 272 | export me $me 273 | export name: method () { 274 | return 'z' 275 | } 276 | export you: method () { 277 | x me # Redirection. 278 | } 279 | } 280 | 281 | z me 282 | z you 283 | 284 | produces the output, 285 | 286 | my name is: z 287 | my name is: x 288 | 289 | #### Syntax 290 | 291 | Oh can be extended with the `syntax` command. The `syntax` command is 292 | very similar to the `method` command except that the methods it creates 293 | are passed their arguments unevaluated. The `eval` command can be used 294 | to explicitly evaluate arguments. A name may be specified for the calling 295 | environment after the list of arguments. This can then be used to 296 | evaluate arguments in the calling environment. 297 | 298 | The example below uses the `syntax` command to define a new `until` command. 299 | 300 | define until: syntax (condition (body)) e { 301 | e eval (cons while (cons (list not $condition) $body)) 302 | } 303 | 304 | define x 0 305 | until (eq? 10 $x) { 306 | echo $x 307 | set x: add $x 1 308 | } 309 | 310 | ### Maps 311 | 312 | Using oh's map type, it is relatively simple to record the exit status 313 | for each stage in a pipeline. The code below, 314 | 315 | define exit-status: map 316 | 317 | define pipe-fitting: method (label (cmd)) e { 318 | exit-status set $label (e eval $cmd) 319 | } 320 | 321 | pipe-fitting 1st echo 1 2 3 | 322 | pipe-fitting 2nd tr ' ' '\n' | 323 | pipe-fitting 3rd grep 2 | 324 | pipe-fitting 4th grep 3 325 | 326 | echo '1st stage exit status =>' (exit-status get 1st) 327 | echo '2nd stage exit status =>' (exit-status get 2nd) 328 | echo '3rd stage exit status =>' (exit-status get 3rd) 329 | echo '4th stage exit status =>' (exit-status get 4th) 330 | 331 | produces the output, 332 | 333 | 1st stage exit status => 0 334 | 2nd stage exit status => 0 335 | 3rd stage exit status => 0 336 | 4th stage exit status => 1 337 | 338 | ### Channels 339 | 340 | Oh exposes channels as first-class values. Channels allow particularly 341 | elegant solutions to some problems, as shown in the prime sieve example 342 | below (adapted from "Newsqueak: A Language for Communicating with Mice"). 343 | 344 | 345 | define filter: method (base) { 346 | mill (n) { 347 | mod $n $base && write $n 348 | } 349 | } 350 | 351 | define connector: chan 352 | 353 | spawn { 354 | define n: number 1 355 | while true { 356 | write (set n: add $n 1) 357 | } 358 | } >$connector 359 | 360 | define prime-numbers: chan 361 | 362 | while true { 363 | define prime: connector read 364 | write $prime 365 | 366 | define filtered: chan 367 | spawn { 368 | filter $prime 369 | } <$connector >$filtered 370 | 371 | set connector $filtered 372 | } >$prime-numbers & 373 | 374 | 375 | define count: number 100 376 | printf "The first %d prime numbers\n" $count 377 | 378 | define line '' 379 | while $count { 380 | define p: prime-numbers read 381 | 382 | set line: mend '' $line (str format "%7.7s" $p) 383 | 384 | set count: sub $count 1 385 | mod $count 10 || block { 386 | echo $line 387 | set line '' 388 | } 389 | } 390 | 391 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/michaelmacinnis/oh 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 7 | github.com/mattn/go-isatty v0.0.19 8 | github.com/michaelmacinnis/adapted v0.7.1 9 | github.com/peterh/liner v1.2.2 10 | golang.org/x/sys v0.12.0 11 | ) 12 | 13 | require ( 14 | github.com/mattn/go-runewidth v0.0.15 // indirect 15 | github.com/rivo/uniseg v0.4.4 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= 2 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 3 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 4 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 5 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 6 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 7 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 8 | github.com/michaelmacinnis/adapted v0.7.1 h1:JHps5RfOZczxyoFPK6obZVJKdnlIR/Td8EE2yDGIt40= 9 | github.com/michaelmacinnis/adapted v0.7.1/go.mod h1:4LFnJK43Kd960P/aQEELLfz66Bw6dtRNeEwBIqXneTg= 10 | github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= 11 | github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= 12 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 13 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 14 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 15 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 18 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package common defines common interfaces 4 | package common 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 10 | ) 11 | 12 | // Error wraps a string as an error. 13 | type Error string 14 | 15 | // Error returns the error string for the Error e. 16 | func (e Error) Error() string { 17 | return string(e) 18 | } 19 | 20 | // String returns the string value for a cell, if possible. 21 | func String(c cell.I) string { 22 | b, ok := c.(fmt.Stringer) 23 | if !ok { 24 | panic(c.Name() + " cannot be used in a string context") 25 | } 26 | 27 | return b.String() 28 | } 29 | -------------------------------------------------------------------------------- /internal/common/interface/boolean/boolean.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package boolean defines the interface for oh types that have a boolean value. 4 | package boolean 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | ) 10 | 11 | // I (boolean) is anything that evaluates to a true or false value. 12 | type I interface { 13 | Bool() bool 14 | } 15 | 16 | // Value returns the boolean value for a cell. 17 | func Value(c cell.I) bool { 18 | b, ok := c.(I) 19 | if !ok { 20 | return c != pair.Null 21 | } 22 | 23 | return b.Bool() 24 | } 25 | -------------------------------------------------------------------------------- /internal/common/interface/cell/cell.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package cell defines the interface for all oh types. 4 | package cell 5 | 6 | // I (cell) is the basic unit of storage in oh. 7 | type I interface { 8 | Equal(c I) bool 9 | Name() string 10 | } 11 | -------------------------------------------------------------------------------- /internal/common/interface/conduit/conduit.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package conduit defines the interface for oh channels and pipes. 4 | package conduit 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | ) 9 | 10 | // I (conduit) is the interface oh channels and pipes satisfy. 11 | type I interface { 12 | cell.I 13 | 14 | Close() 15 | Read() cell.I 16 | ReadLine() cell.I 17 | ReaderClose() 18 | Write(v cell.I) 19 | WriteLine(v cell.I) 20 | WriterClose() 21 | } 22 | 23 | type conduit = I 24 | 25 | // Is returns true if c is an I. 26 | func Is(c cell.I) bool { 27 | _, ok := c.(conduit) 28 | 29 | return ok 30 | } 31 | 32 | // To returns a I if c is a I; Otherwise it panics. 33 | func To(c cell.I) I { 34 | if t, ok := c.(conduit); ok { 35 | return t 36 | } 37 | 38 | panic("not a conduit") 39 | } 40 | -------------------------------------------------------------------------------- /internal/common/interface/integer/integer.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package integer converts an oh cell to an int64 value, if possible. 4 | package integer 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 11 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 12 | ) 13 | 14 | // Value returns the int64 value for a cell, if possible. 15 | func Value(c cell.I) int64 { 16 | r, isRational := c.(rational.I) 17 | if isRational { 18 | br := r.Rat() 19 | if br.IsInt() { 20 | bi := br.Num() 21 | if bi.IsInt64() { 22 | return bi.Int64() 23 | } 24 | } 25 | 26 | panic(c.Name() + " does not have an integer value") 27 | } 28 | 29 | s, isSym := c.(*sym.T) 30 | if isSym { 31 | i, err := strconv.ParseInt(s.String(), 10, 64) 32 | if err != nil { 33 | return i 34 | } 35 | } 36 | 37 | panic(c.Name() + " cannot be converted to an integer value") 38 | } 39 | -------------------------------------------------------------------------------- /internal/common/interface/literal/literal.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package literal defines the interface for oh types that can be expressed as literals. 4 | package literal 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | ) 9 | 10 | // I (literal) is any type that can be expressed as a literal. 11 | type I interface { 12 | Literal() string 13 | } 14 | 15 | // String returns the literal string representaition for a cell, if possible. 16 | func String(c cell.I) string { 17 | l, ok := c.(I) 18 | if !ok { 19 | // Not all cell types can be expressed as literals. 20 | panic(c.Name() + " does not have a literal representation") 21 | } 22 | 23 | return l.Literal() 24 | } 25 | -------------------------------------------------------------------------------- /internal/common/interface/rational/rational.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package rational defines the interface for oh's numeric types. 4 | package rational 5 | 6 | import ( 7 | "math/big" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 10 | ) 11 | 12 | // I (rational) is anything that can be treated as a rational number in oh. 13 | type I interface { 14 | Rat() *big.Rat 15 | } 16 | 17 | type rational = I 18 | 19 | // Number returns the *big.Rat value for a cell, if possible. 20 | func Number(c cell.I) *big.Rat { 21 | r, ok := c.(rational) 22 | if !ok { 23 | // Not all cell types can be treated as numbers. 24 | panic(c.Name() + " cannot be use in a numeric context") 25 | } 26 | 27 | return r.Rat() 28 | } 29 | -------------------------------------------------------------------------------- /internal/common/interface/reference/reference.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package reference defines the interface for oh's variable type. 4 | package reference 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | ) 9 | 10 | // I (reference) is anything that can hold a value. 11 | type I interface { 12 | Copy() I 13 | Get() cell.I 14 | Set(cell.I) 15 | } 16 | -------------------------------------------------------------------------------- /internal/common/interface/scope/scope.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package scope defines the interface for oh's first-class environments and objects. 4 | package scope 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 9 | "github.com/michaelmacinnis/oh/internal/common/struct/hash" 10 | ) 11 | 12 | // I (scope) is the interface for oh's first-class environments and objects. 13 | type I interface { 14 | cell.I 15 | 16 | Clone() I 17 | Enclosing() I 18 | Expose() I 19 | 20 | Define(k string, v cell.I) 21 | Export(k string, v cell.I) 22 | Lookup(k string) reference.I 23 | Public() *hash.T 24 | Remove(k string) bool 25 | 26 | Exported() int 27 | Visible(o I) bool 28 | } 29 | 30 | type scope = I 31 | 32 | // Is returns true if c is a scope. 33 | func Is(c cell.I) bool { 34 | _, ok := c.(scope) 35 | 36 | return ok 37 | } 38 | 39 | // To returns a scope if c is a scope; Otherwise it panics. 40 | func To(c cell.I) scope { 41 | if t, ok := c.(scope); ok { 42 | return t 43 | } 44 | 45 | panic(c.Name() + " cannot be used in an object context") 46 | } 47 | -------------------------------------------------------------------------------- /internal/common/struct/frame/frame.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package frame provides oh's call stack frame type. 4 | package frame 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 8 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 9 | "github.com/michaelmacinnis/oh/internal/common/struct/loc" 10 | ) 11 | 12 | // T (frame) is stack frame or activation record. 13 | type T struct { 14 | previous *frame 15 | scope scope.I 16 | source loc.T 17 | } 18 | 19 | type frame = T 20 | 21 | // Dup creates a duplicate of the frame f with a new scope s. 22 | func Dup(s scope.I, f *frame) *frame { 23 | dup := *f 24 | dup.scope = s 25 | 26 | return &dup 27 | } 28 | 29 | // New creates a new frame with the scope s and previous frame p. 30 | func New(s scope.I, p *frame) *frame { 31 | f := &frame{scope: s} 32 | 33 | if p != nil { 34 | f.previous = p 35 | f.source = p.source 36 | } 37 | 38 | return f 39 | } 40 | 41 | // Loc returns the current location. 42 | func (f *frame) Loc() *loc.T { 43 | return &f.source 44 | } 45 | 46 | // Previous returns the previous frame. 47 | func (f *frame) Previous() *frame { 48 | return f.previous 49 | } 50 | 51 | // Resolve looks for a lexical and then dynamic resolution of k. 52 | // The scope where the reference r was found is also returned. 53 | func (f *frame) Resolve(k string) (s scope.I, r reference.I) { 54 | s = f.scope 55 | 56 | r = s.Lookup(k) 57 | if r != nil { 58 | return 59 | } 60 | 61 | for f = f.previous; f != nil; f = f.previous { 62 | for s = f.scope; s != nil; s = s.Enclosing() { 63 | r = s.Public().Get(k) 64 | if r != nil { 65 | return 66 | } 67 | } 68 | } 69 | 70 | return 71 | } 72 | 73 | // Scope returns the current frame's scope. 74 | func (f *frame) Scope() scope.I { 75 | return f.scope 76 | } 77 | 78 | // Update sets the current lexical location. 79 | func (f *frame) Update(source *loc.T) { 80 | f.source = *source 81 | } 82 | -------------------------------------------------------------------------------- /internal/common/struct/hash/hash.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package hash provides oh's name to value mapping type. 4 | package hash 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 12 | "github.com/michaelmacinnis/oh/internal/common/struct/slot" 13 | ) 14 | 15 | // T (hash) maps names to values. 16 | type T struct { 17 | sync.RWMutex 18 | m map[string]reference.I 19 | } 20 | 21 | type hash = T 22 | 23 | // New creates a new hash. 24 | func New() *hash { 25 | return &hash{m: map[string]reference.I{}} 26 | } 27 | 28 | // Copy creates a new hash with a copy of every reference. 29 | func (h *hash) Copy() *hash { 30 | if h == nil { 31 | return nil 32 | } 33 | 34 | h.RLock() 35 | defer h.RUnlock() 36 | 37 | fresh := New() 38 | for k, v := range h.m { 39 | fresh.m[k] = v.Copy() 40 | } 41 | 42 | return fresh 43 | } 44 | 45 | // Del frees the name k from any association in the hash h. 46 | func (h *hash) Del(k string) bool { 47 | if h == nil { 48 | return false 49 | } 50 | 51 | h.Lock() 52 | defer h.Unlock() 53 | 54 | _, ok := h.m[k] 55 | if !ok { 56 | return false 57 | } 58 | 59 | delete(h.m, k) 60 | 61 | return true 62 | } 63 | 64 | // Exported returns a map with containing all entries in h with a string value. 65 | func (h *hash) Exported() map[string]string { 66 | h.Lock() 67 | defer h.Unlock() 68 | 69 | exported := map[string]string{} 70 | 71 | for k, v := range h.m { 72 | if s, ok := v.Get().(fmt.Stringer); ok { 73 | exported[k] = s.String() 74 | } 75 | } 76 | 77 | return exported 78 | } 79 | 80 | // Get retrieves the reference associated with the name k in the hash h. 81 | func (h *hash) Get(k string) reference.I { 82 | if h == nil { 83 | return nil 84 | } 85 | 86 | h.RLock() 87 | defer h.RUnlock() 88 | 89 | return h.m[k] 90 | } 91 | 92 | // Set associates the name k with the cell v in the hash h. 93 | func (h *hash) Set(k string, v cell.I) { 94 | h.Lock() 95 | defer h.Unlock() 96 | 97 | h.m[k] = slot.New(v) 98 | } 99 | 100 | // Size returns the number of entries in the hash h. 101 | func (h *hash) Size() int { 102 | h.RLock() 103 | defer h.RUnlock() 104 | 105 | return len(h.m) 106 | } 107 | -------------------------------------------------------------------------------- /internal/common/struct/loc/loc.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package loc provides the type used to track the source of tokens and commands. 4 | // It is also used to keep track of the evaluator's current lexical location. 5 | package loc 6 | 7 | import ( 8 | "strconv" 9 | ) 10 | 11 | // T (loc) is a lexical location. 12 | type T struct { 13 | Char int // Character position (column). 14 | Line int // Line number (row). 15 | Name string // Label for the source of this token. 16 | Text string // The text at this location. 17 | } 18 | 19 | type loc = T 20 | 21 | func (l loc) Error() string { 22 | return l.String() + " " + l.Text 23 | } 24 | 25 | func (l *loc) String() string { 26 | return l.Name + ":" + strconv.Itoa(l.Line) + ":" + strconv.Itoa(l.Char) 27 | } 28 | -------------------------------------------------------------------------------- /internal/common/struct/slot/slot.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package slot provides oh's variable type. 4 | package slot 5 | 6 | import ( 7 | "sync" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 11 | ) 12 | 13 | // T (slot) holds a cell value. 14 | type T struct { 15 | sync.RWMutex 16 | c cell.I 17 | } 18 | 19 | type slot = T 20 | 21 | // New creates a new slot with the cell c. 22 | func New(c cell.I) *slot { 23 | return &slot{c: c} 24 | } 25 | 26 | // Copy creates a new slot with the same cell as slot s. 27 | func (s *slot) Copy() reference.I { 28 | return New(s.Get()) 29 | } 30 | 31 | // Get returns the cell in slot s. 32 | func (s *slot) Get() cell.I { 33 | s.RLock() 34 | defer s.RUnlock() 35 | 36 | return s.c 37 | } 38 | 39 | // Set replaces the cell in slot s with the cell c. 40 | func (s *slot) Set(c cell.I) { 41 | s.Lock() 42 | defer s.Unlock() 43 | 44 | s.c = c 45 | } 46 | -------------------------------------------------------------------------------- /internal/common/struct/token/token.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package token is shared by the oh lexer and parser. 4 | package token 5 | 6 | import ( 7 | "strconv" 8 | "unicode" 9 | 10 | "github.com/michaelmacinnis/oh/internal/common/struct/loc" 11 | ) 12 | 13 | // Class is a token's type. 14 | type Class rune 15 | 16 | // T (token) is a lexical item returned by the scanner. 17 | type T struct { 18 | class Class 19 | source *loc.T 20 | value string 21 | } 22 | 23 | type token = T 24 | 25 | // Token classes. 26 | const ( 27 | Error Class = iota 28 | 29 | Andf Class = unicode.MaxRune + iota 30 | Background 31 | DollarSingleQuoted 32 | DoubleQuoted 33 | MetaClose 34 | MetaOpen 35 | Orf 36 | Pipe 37 | Redirect 38 | SingleQuoted 39 | Space 40 | Substitute 41 | Symbol 42 | ) 43 | 44 | // New creates a new token. 45 | func New(class Class, value string, source *loc.T) *token { 46 | return &token{ 47 | class: class, 48 | source: source, 49 | value: value, 50 | } 51 | } 52 | 53 | // String returns a string representation of Class. Useful for debugging. 54 | func (c *Class) String() string { 55 | switch *c { 56 | case Error: 57 | return "Error" 58 | case Andf: 59 | return "Andf" 60 | case Background: 61 | return "Background" 62 | case DollarSingleQuoted: 63 | return "DollarSingleQuoted" 64 | case DoubleQuoted: 65 | return "DoubleQuoted" 66 | case MetaClose: 67 | return "MetaClose" 68 | case MetaOpen: 69 | return "MetaOpen" 70 | case Orf: 71 | return "Orf" 72 | case Pipe: 73 | return "Pipe" 74 | case Redirect: 75 | return "Redirect" 76 | case SingleQuoted: 77 | return "SingleQuoted" 78 | case Space: 79 | return "Space" 80 | case Substitute: 81 | return "Substitute" 82 | case Symbol: 83 | return "Symbol" 84 | } 85 | 86 | return strconv.QuoteRune(rune(*c)) 87 | } 88 | 89 | // Is returns true if the token t is any of the classes in cs. 90 | func (t *token) Is(cs ...Class) bool { 91 | if t == nil { 92 | return false 93 | } 94 | 95 | for _, c := range cs { 96 | if t.class == c { 97 | return true 98 | } 99 | } 100 | 101 | return false 102 | } 103 | 104 | // Source returns the source location for this token. 105 | func (t *token) Source() *loc.T { 106 | return t.source 107 | } 108 | 109 | // String returns the token's string representation. Useful for debugging. 110 | func (t *token) String() string { 111 | return strconv.Quote(t.value) + "(" + 112 | t.class.String() + "," + 113 | t.source.String() + ")" 114 | } 115 | 116 | // Value returns the token's string value. 117 | func (t *token) Value() string { 118 | return t.value 119 | } 120 | -------------------------------------------------------------------------------- /internal/common/type/chn/chn.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package chn provides oh's channel type. 4 | package chn 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/michaelmacinnis/adapted" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/conduit" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 13 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 14 | "github.com/michaelmacinnis/oh/internal/common/type/str" 15 | ) 16 | 17 | const name = "chan" 18 | 19 | // T (chn) is oh's channel conduit type. 20 | type T chan cell.I 21 | 22 | type chn = T 23 | 24 | // New creates a new chn cell. 25 | func New(cap int64) cell.I { 26 | c := chn(make(chan cell.I, cap)) 27 | 28 | return &c 29 | } 30 | 31 | // Close closes the chn. 32 | func (c *chn) Close() { 33 | c.WriterClose() 34 | } 35 | 36 | // Equal returns true if the cell c is the same chn and false otherwise. 37 | func (c *chn) Equal(v cell.I) bool { 38 | return Is(v) && c == To(v) 39 | } 40 | 41 | // Name returns the name of the chn type. 42 | func (*chn) Name() string { 43 | return name 44 | } 45 | 46 | // Read reads a cell from the chn. 47 | func (c *chn) Read() cell.I { 48 | v := <-*c 49 | if v == nil { 50 | return pair.Null 51 | } 52 | 53 | return v 54 | } 55 | 56 | // ReadLine reads a line from the chn. 57 | func (c *chn) ReadLine() cell.I { 58 | v := <-*c 59 | if v == nil { 60 | return pair.Null 61 | } 62 | 63 | i, ok := v.(fmt.Stringer) 64 | if ok { 65 | s, err := adapted.ActualBytes(i.String()) 66 | if err == nil { 67 | return str.New(s) 68 | } 69 | } 70 | 71 | return str.New(literal.String(v)) 72 | } 73 | 74 | // ReaderClose is a no-op for a chn. 75 | func (c *chn) ReaderClose() {} 76 | 77 | // Write writes a cell to the chn. 78 | func (c *chn) Write(v cell.I) { 79 | *c <- v 80 | } 81 | 82 | // WriteLine writes a cell to the chn. 83 | func (c *chn) WriteLine(v cell.I) { 84 | *c <- v 85 | } 86 | 87 | // WriterClose closes the chn. 88 | func (c *chn) WriterClose() { 89 | close(*c) 90 | } 91 | 92 | // A compiler-checked list of interfaces this type satisfies. Never called. 93 | func implements() { //nolint:deadcode,unused 94 | var t chn 95 | 96 | // The chn type is a cell. 97 | _ = cell.I(&t) 98 | 99 | // The chn type is a conduit. 100 | _ = conduit.I(&t) 101 | } 102 | -------------------------------------------------------------------------------- /internal/common/type/chn/chn_internal_test.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package chn 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/michaelmacinnis/oh/internal/common/type/str" 9 | ) 10 | 11 | func TestWriteRead(t *testing.T) { 12 | p := New(1).(*chn) 13 | 14 | sent := str.New("hello") 15 | 16 | p.Write(sent) 17 | 18 | received := p.Read() 19 | 20 | if !received.Equal(sent) { 21 | t.Fail() 22 | } 23 | } 24 | 25 | func TestWriteReadLine(t *testing.T) { 26 | p := New(1).(*chn) 27 | 28 | sent := str.New("hello") 29 | 30 | p.Write(sent) 31 | 32 | received := p.ReadLine() 33 | 34 | if !received.Equal(sent) { 35 | t.Fail() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/common/type/chn/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package chn 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/create/create.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package create provides helper functions for creating oh types. 4 | package create 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 10 | ) 11 | 12 | // Bool returns the oh value corresponding to the value of the boolean a. 13 | func Bool(a bool) cell.I { 14 | if a { 15 | return sym.True 16 | } 17 | 18 | return pair.Null 19 | } 20 | -------------------------------------------------------------------------------- /internal/common/type/env/env.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package env provides oh's first-class environment type. 4 | package env 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 9 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 10 | "github.com/michaelmacinnis/oh/internal/common/struct/hash" 11 | ) 12 | 13 | const name = "environment" 14 | 15 | // T (env) provides a public and private mapping of names to values. 16 | type T struct { 17 | previous scope.I 18 | private *hash.T 19 | public *hash.T 20 | } 21 | 22 | type env = T 23 | 24 | // New creates a new env. 25 | func New(previous scope.I) scope.I { 26 | return &env{ 27 | previous: previous, 28 | private: hash.New(), 29 | public: hash.New(), 30 | } 31 | } 32 | 33 | // Clone creates a clone of the current scope. 34 | func (e *env) Clone() scope.I { 35 | return &env{ 36 | previous: e.previous, 37 | private: e.private.Copy(), 38 | public: e.public.Copy(), 39 | } 40 | } 41 | 42 | // Define associates the private name k with the cell v in the env e. 43 | func (e *env) Define(k string, v cell.I) { 44 | e.private.Set(k, v) 45 | } 46 | 47 | // Enclosing returns the enclosing scope. 48 | func (e *env) Enclosing() scope.I { 49 | return e.previous 50 | } 51 | 52 | // Equal returns true if c is the same env as e. 53 | func (e *env) Equal(c cell.I) bool { 54 | return Is(c) && e == To(c) 55 | } 56 | 57 | // Export associates the public name k with the cell v in the env e. 58 | func (e *env) Export(k string, v cell.I) { 59 | e.public.Set(k, v) 60 | } 61 | 62 | // Exported returns the number of exported variables. 63 | func (e *env) Exported() int { 64 | return e.public.Size() 65 | } 66 | 67 | // Expose returns a scope with public and private members visible. 68 | func (e *env) Expose() scope.I { 69 | return e 70 | } 71 | 72 | // Lookup retrieves the reference associated with the name k in the env e. 73 | func (e *env) Lookup(k string) reference.I { 74 | if e == nil { 75 | return nil 76 | } 77 | 78 | v := e.private.Get(k) 79 | 80 | if v == nil { 81 | v = e.public.Get(k) 82 | } 83 | 84 | if v == nil && e.previous != nil { 85 | v = e.previous.Lookup(k) 86 | } 87 | 88 | return v 89 | } 90 | 91 | // Name returns the type name for the env e. 92 | func (e *env) Name() string { 93 | return name 94 | } 95 | 96 | // Public returns the public hash for the env e. 97 | func (e *env) Public() *hash.T { 98 | return e.public 99 | } 100 | 101 | // Remove deletes the name k from the env e. 102 | func (e *env) Remove(k string) bool { 103 | if e == nil { 104 | return false 105 | } 106 | 107 | if e.private.Del(k) || e.public.Del(k) { 108 | return true 109 | } 110 | 111 | parent := e.Enclosing() 112 | if parent == nil { 113 | return false 114 | } 115 | 116 | return parent.Remove(k) 117 | } 118 | 119 | // Visible returns true if exported variables in o are visible in e. 120 | func (e *env) Visible(o scope.I) bool { 121 | for o != nil && o.Exported() == 0 { 122 | o = o.Enclosing() 123 | } 124 | 125 | if o == nil { 126 | return true 127 | } 128 | 129 | p := o.Expose() 130 | 131 | o = e 132 | for o != nil && o.Exported() == 0 { 133 | o = o.Enclosing() 134 | } 135 | 136 | if o == nil { 137 | return false 138 | } 139 | 140 | return p == o.Expose() 141 | } 142 | 143 | // A compiler-checked list of interfaces this type satisfies. Never called. 144 | func implements() { //nolint:deadcode,unused 145 | var t env 146 | 147 | // The env type is a cell. 148 | _ = cell.I(&t) 149 | 150 | // The env type is a scope. 151 | _ = scope.I(&t) 152 | } 153 | -------------------------------------------------------------------------------- /internal/common/type/env/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package env 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/list/list.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package list provides common list operations. A list is not a true type. 4 | // Lists are more of a type by convention. They are composed of cons cells. 5 | package list 6 | 7 | import ( 8 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 9 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 10 | ) 11 | 12 | // Append appends each element in elements to list. 13 | // If list is Null, a new list is created. 14 | // A non-pair value where a pair is expected will cause a panic. 15 | // The list must be non-circular. 16 | func Append(start cell.I, elements ...cell.I) cell.I { 17 | if start == nil { 18 | panic("cannot append to non-existent list") 19 | } 20 | 21 | if len(elements) == 0 { 22 | return start 23 | } 24 | 25 | if start == pair.Null { 26 | start = pair.Cons(elements[0], pair.Null) 27 | elements = elements[1:] 28 | } 29 | 30 | var end cell.I 31 | for list := start; list != pair.Null; list = pair.Cdr(list) { 32 | end = list 33 | } 34 | 35 | for _, e := range elements { 36 | p := pair.Cons(e, pair.Null) 37 | pair.SetCdr(end, p) 38 | end = p 39 | } 40 | 41 | return start 42 | } 43 | 44 | // Array returns an array with the elements in list. 45 | // A non-pair value where a pair is expected will cause a panic. 46 | // The list must be non-circular. 47 | func Array(list cell.I) []cell.I { 48 | a := []cell.I{} 49 | 50 | for c := list; c != pair.Null; c = pair.Cdr(c) { 51 | a = append(a, pair.Car(c)) 52 | } 53 | 54 | return a 55 | } 56 | 57 | // Join extends the first non-nil, non-NULL list in list 58 | // with every element from every list remaining in lists. 59 | // A non-pair where a pair is expected will cause a panic. 60 | // All lists must be non-circular. 61 | func Join(lists ...cell.I) cell.I { 62 | var end, start cell.I 63 | 64 | // Find the first non-nil, non-Null list, start. 65 | for len(lists) != 0 { 66 | start = lists[0] 67 | lists = lists[1:] 68 | 69 | if start != nil && start != pair.Null { 70 | break 71 | } 72 | } 73 | 74 | if start == nil { 75 | panic("join must be passed at least one list") 76 | } 77 | 78 | if start == pair.Null { 79 | return start 80 | } 81 | 82 | // Find the end of the list start. 83 | for list := start; list != pair.Null; list = pair.Cdr(list) { 84 | end = list 85 | } 86 | 87 | for _, list := range lists { 88 | if list == nil { 89 | continue 90 | } 91 | 92 | for list != pair.Null { 93 | p := pair.Cons(pair.Car(list), pair.Null) 94 | pair.SetCdr(end, p) 95 | end = p 96 | 97 | list = pair.Cdr(list) 98 | } 99 | } 100 | 101 | return start 102 | } 103 | 104 | // Length returns the number of elements in list. 105 | // A non-pair value where a pair is expected will cause a panic. 106 | // The list must be non-circular. 107 | func Length(list cell.I) int64 { 108 | var length int64 109 | 110 | for list != nil && list != pair.Null { 111 | length++ 112 | 113 | list = pair.Cdr(list) 114 | } 115 | 116 | return length 117 | } 118 | 119 | // New creates a new list composed of all of the elements in elements. 120 | func New(elements ...cell.I) cell.I { 121 | if len(elements) == 0 { 122 | return pair.Null 123 | } 124 | 125 | start := pair.Cons(elements[0], pair.Null) 126 | end := start 127 | 128 | for _, e := range elements[1:] { 129 | p := pair.Cons(e, pair.Null) 130 | pair.SetCdr(end, p) 131 | end = p 132 | } 133 | 134 | return start 135 | } 136 | 137 | // Reverse reverses list. 138 | // A non-pair value where a pair is expected will cause a panic. 139 | // The list must be non-circular. 140 | func Reverse(list cell.I) cell.I { 141 | reversed := pair.Null 142 | 143 | for list != nil && list != pair.Null { 144 | reversed = pair.Cons(pair.Car(list), reversed) 145 | 146 | list = pair.Cdr(list) 147 | } 148 | 149 | return reversed 150 | } 151 | 152 | // Slice creates a new list that is a slice of list. 153 | // Start must be non-zero. If start is > length it will be set to length. 154 | // End must be >= -length. If end is > length it will be set to length. 155 | // Negative values of end count backwards from the end of list. 156 | // Invalid start or end values will cause this function to panic. 157 | // A non-pair value where a pair is expected will cause a panic. 158 | // The list must be non-circular. 159 | func Slice(list cell.I, start, end int64) cell.I { 160 | length := Length(list) 161 | 162 | if start < 0 { 163 | start = length + start 164 | } 165 | 166 | if start < 0 { 167 | panic("slice starts before first element") 168 | } else if start > length { 169 | start = length 170 | } 171 | 172 | if end <= 0 { 173 | end = length + end 174 | } 175 | 176 | if end < 0 { 177 | panic("slice ends before first element") 178 | } else if end > length { 179 | end = length 180 | } 181 | 182 | end -= start 183 | 184 | if end < 0 { 185 | panic("end of slice before start") 186 | } else if end == 0 { 187 | return pair.Null 188 | } 189 | 190 | for start > 0 { 191 | list = pair.Cdr(list) 192 | 193 | start-- 194 | } 195 | 196 | slice := pair.Cons(pair.Car(list), pair.Null) 197 | 198 | for c := slice; end > 1; end-- { 199 | list = pair.Cdr(list) 200 | n := pair.Cons(pair.Car(list), pair.Null) 201 | pair.SetCdr(c, n) 202 | c = n 203 | } 204 | 205 | return slice 206 | } 207 | 208 | // Tail returns the sublist of list starting at element index. 209 | // Negative values of index count backwards from the end of list. 210 | // If index is out of range and dflt is provided it is returned. 211 | // Otherwise, this function panics. 212 | // A non-pair value where a pair is expected will cause a panic. 213 | // The list must be non-circular. 214 | func Tail(list cell.I, index int64, dflt cell.I) cell.I { 215 | length := Length(list) 216 | 217 | if index < 0 { 218 | index = length + index 219 | } 220 | 221 | msg := "" 222 | if index < 0 { 223 | msg = "index before first element" 224 | } else if index >= length { 225 | msg = "index after last element" 226 | } 227 | 228 | if msg != "" { 229 | if dflt == nil { 230 | panic(msg) 231 | } else { 232 | return dflt 233 | } 234 | } 235 | 236 | for index > 0 { 237 | list = pair.Cdr(list) 238 | 239 | index-- 240 | } 241 | 242 | return list 243 | } 244 | -------------------------------------------------------------------------------- /internal/common/type/num/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package num 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/num/num.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package num provides oh's rational number type. 4 | package num 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/michaelmacinnis/oh/internal/common/interface/boolean" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 14 | ) 15 | 16 | const name = "number" 17 | 18 | // T (num) wraps Go's big.Rat type. 19 | type T big.Rat 20 | 21 | type num = T 22 | 23 | // Int creates a num from the integer i. 24 | func Int(i int) cell.I { 25 | return Rat(big.NewRat(int64(i), 1)) 26 | } 27 | 28 | // New creates a new num from a string. 29 | func New(s string) cell.I { 30 | v := &big.Rat{} 31 | 32 | if _, ok := v.SetString(s); !ok { 33 | panic("'" + s + "' is not a valid number") 34 | } 35 | 36 | return Rat(v) 37 | } 38 | 39 | // Rat creates wraps the *big.Rat r as a num. 40 | func Rat(r *big.Rat) cell.I { 41 | return (*num)(r) 42 | } 43 | 44 | // Bool returns the boolean value of the num n. 45 | func (n *num) Bool() bool { 46 | return n.Rat().Cmp(&big.Rat{}) != 0 47 | } 48 | 49 | // Equal returns true if c is the same number as the num n. 50 | func (n *num) Equal(c cell.I) bool { 51 | return Is(c) && n.Rat().Cmp(To(c).Rat()) == 0 52 | } 53 | 54 | // Literal returns the literal representation of the num n. 55 | func (n *num) Literal() string { 56 | return "(|" + name + " " + n.String() + "|)" 57 | } 58 | 59 | // Name returns the type name for the num n. 60 | func (n *num) Name() string { 61 | return name 62 | } 63 | 64 | // Rat returns the value of the num n as a *big.Rat. 65 | func (n *num) Rat() *big.Rat { 66 | return (*big.Rat)(n) 67 | } 68 | 69 | // String returns the text of the num n. 70 | func (n *num) String() string { 71 | return n.Rat().RatString() 72 | } 73 | 74 | // A compiler-checked list of interfaces this type satisfies. Never called. 75 | func implements() { //nolint:deadcode,unused 76 | var t num 77 | 78 | // The num type has a boolean value. 79 | _ = boolean.I(&t) 80 | 81 | // The num type is a cell. 82 | _ = cell.I(&t) 83 | 84 | // The num type has a literal representation. 85 | _ = literal.I(&t) 86 | 87 | // The num type is a rational. 88 | _ = rational.I(&t) 89 | 90 | // The num type is a stringer. 91 | _ = fmt.Stringer(&t) 92 | } 93 | -------------------------------------------------------------------------------- /internal/common/type/obj/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package obj 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/obj/obj.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package obj provides oh's object type. 4 | package obj 5 | 6 | import ( 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 9 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 10 | ) 11 | 12 | const name = "object" 13 | 14 | // NOTE: This is not how objects were implemented in the previous version 15 | // of oh but I think this might be closer to what we want. Shared behavior 16 | // will need to be explicitly "pulled up" by associating a (public or 17 | // private) name with a value in an enclosing scope. But the nice thing is 18 | // that is will stop objects from being peepholes into the public scope 19 | // of their creation. Or that is my current thinking. Let's see what I'm 20 | // forgetting about that this breaks. 21 | 22 | // T (object) limits access to top-level, public names in the wrapped env. 23 | type T struct { 24 | wrapped 25 | } 26 | 27 | type obj = T 28 | 29 | type wrapped = scope.I 30 | 31 | // New creates a new obj. 32 | func New(e scope.I) scope.I { 33 | return &obj{e} 34 | } 35 | 36 | // Clone creates a clone of the obj o. 37 | func (o *obj) Clone() scope.I { 38 | return &obj{o.wrapped.Clone()} 39 | } 40 | 41 | // Define throws an error. Only public members of an obj can be added. 42 | func (o *obj) Define(k string, v cell.I) { 43 | panic("private names cannot be added to object") 44 | } 45 | 46 | // Equal returns true if c is obj as o. 47 | func (o *obj) Equal(c cell.I) bool { 48 | return Is(c) && o == To(c) 49 | } 50 | 51 | // Expose returns the wrapped env. 52 | func (o *obj) Expose() scope.I { 53 | return o.wrapped 54 | } 55 | 56 | // Lookup retrieves the reference associated with the public name k in the obj o. 57 | func (o *obj) Lookup(k string) reference.I { 58 | return o.wrapped.Public().Get(k) 59 | } 60 | 61 | // Name returns the type name for the obj o. 62 | func (o *obj) Name() string { 63 | return name 64 | } 65 | 66 | // Remove frees the public name k from any association in the obj o. 67 | func (o *obj) Remove(k string) bool { 68 | return o.wrapped.Public().Del(k) 69 | } 70 | 71 | // A compiler-checked list of interfaces this type satisfies. Never called. 72 | func implements() { //nolint:deadcode,unused 73 | var t obj 74 | 75 | // The obj type is a cell. 76 | _ = cell.I(&t) 77 | 78 | // The obj type is a scope. 79 | _ = scope.I(&t) 80 | } 81 | -------------------------------------------------------------------------------- /internal/common/type/pair/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package pair 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/pair/pair.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package pair provides oh's cons cell type. 4 | package pair 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 12 | ) 13 | 14 | const name = "cons" 15 | 16 | //nolint:gochecknoglobals 17 | var ( 18 | // Null is the empty list. It is also used to mark the end of a list. 19 | Null cell.I 20 | ) 21 | 22 | // T (pair) is a cons cell. 23 | type T struct { 24 | car cell.I 25 | cdr cell.I 26 | } 27 | 28 | type pair = T 29 | 30 | // Equal returns true if c is a pair with elements that are equal to p's. 31 | func (p *pair) Equal(c cell.I) bool { 32 | if p == Null && c == Null { 33 | return true 34 | } 35 | 36 | return p.car.Equal(Car(c)) && p.cdr.Equal(Cdr(c)) 37 | } 38 | 39 | // Literal returns the literal representation of the pair p. 40 | func (p *pair) Literal() string { 41 | return p.string(literal.String) 42 | } 43 | 44 | // Name returns the name for a pair type. 45 | func (p *pair) Name() string { 46 | return name 47 | } 48 | 49 | // String returns the text representation of the pair p. 50 | func (p *pair) String() string { 51 | return p.string(common.String) 52 | } 53 | 54 | func (p *pair) string(toString func(cell.I) string) string { 55 | s := "" 56 | 57 | improper := false 58 | 59 | tail := Cdr(p) 60 | if !Is(tail) { 61 | improper = true 62 | s += "(|" + name + " " 63 | } 64 | 65 | sublist := false 66 | 67 | head := Car(p) 68 | if Is(head) && Is(Cdr(head)) { 69 | sublist = true 70 | s += "(" 71 | } 72 | 73 | if head == nil { 74 | s += "()" 75 | } else if head != Null { 76 | s += toString(head) 77 | } 78 | 79 | if sublist { 80 | s += ")" 81 | } 82 | 83 | if !improper && tail == Null { 84 | return s 85 | } 86 | 87 | s += " " 88 | if tail == nil { 89 | s += "()" 90 | } else { 91 | s += toString(tail) 92 | } 93 | 94 | if improper { 95 | s += "|)" 96 | } 97 | 98 | return s 99 | } 100 | 101 | // Functions specific to pair. 102 | 103 | // Car returns the car/head/first member of the pair c. 104 | // If c is not a pair, this function will panic. 105 | func Car(c cell.I) cell.I { 106 | return To(c).car 107 | } 108 | 109 | // Cdr returns the cdr/tail/rest member of the pair c. 110 | // If c is not a pair, this function will panic. 111 | func Cdr(c cell.I) cell.I { 112 | return To(c).cdr 113 | } 114 | 115 | // Caar returns the car of the car of the pair c. 116 | // A non-pair value where a pair is expected will cause a panic. 117 | func Caar(c cell.I) cell.I { 118 | return To(To(c).car).car 119 | } 120 | 121 | // Cadr returns the car of the cdr of the pair c. 122 | // A non-pair value where a pair is expected will cause a panic. 123 | func Cadr(c cell.I) cell.I { 124 | return To(To(c).cdr).car 125 | } 126 | 127 | // Cdar returns the cdr of the car of the pair c. 128 | // A non-pair value where a pair is expected will cause a panic. 129 | func Cdar(c cell.I) cell.I { 130 | return To(To(c).car).cdr 131 | } 132 | 133 | // Cddr returns the cdr of the cdr of the pair c. 134 | // A non-pair value where a pair is expected will cause a panic. 135 | func Cddr(c cell.I) cell.I { 136 | return To(To(c).cdr).cdr 137 | } 138 | 139 | // Caddr returns the car of the cdr of the cdr of the pair c. 140 | // A non-pair value where a pair is expected will cause a panic. 141 | func Caddr(c cell.I) cell.I { 142 | return To(To(To(c).cdr).cdr).car 143 | } 144 | 145 | // Cons conses h and t together to form a new pair. 146 | func Cons(h, t cell.I) cell.I { 147 | return &pair{car: h, cdr: t} 148 | } 149 | 150 | // SetCar sets the car/head/first of the pair c to value. 151 | // If c is not a pair, this function will panic. 152 | func SetCar(c, value cell.I) { 153 | To(c).car = value 154 | } 155 | 156 | // SetCdr sets the cdr/tail/rest of the pair c to value. 157 | // If c is not a pair, this function will panic. 158 | func SetCdr(c, value cell.I) { 159 | To(c).cdr = value 160 | } 161 | 162 | // A compiler-checked list of interfaces this type satisfies. Never called. 163 | func implements() { //nolint:deadcode,unused 164 | var t pair 165 | 166 | // The pair type is a cell. 167 | _ = cell.I(&t) 168 | 169 | // The pair type has a literal representation. 170 | _ = literal.I(&t) 171 | 172 | // The pair type is a stringer. 173 | _ = fmt.Stringer(&t) 174 | } 175 | 176 | func init() { //nolint:gochecknoinits 177 | pair := &pair{} 178 | pair.car = pair 179 | pair.cdr = pair 180 | 181 | Null = cell.I(pair) 182 | } 183 | -------------------------------------------------------------------------------- /internal/common/type/pipe/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package pipe 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/pipe/pipe.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package pipe provides oh's pipe type. 4 | package pipe 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "io" 10 | "os" 11 | "runtime" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/michaelmacinnis/oh/internal/common" 16 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 17 | "github.com/michaelmacinnis/oh/internal/common/interface/conduit" 18 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 19 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 20 | "github.com/michaelmacinnis/oh/internal/common/type/str" 21 | "github.com/michaelmacinnis/oh/internal/reader" 22 | ) 23 | 24 | const name = "pipe" 25 | 26 | // T (pipe) is oh's pipe conduit type. 27 | type T struct { 28 | sync.RWMutex 29 | b *bufio.Reader 30 | p *reader.T 31 | r *os.File 32 | w *os.File 33 | } 34 | 35 | type pipe = T 36 | 37 | // New creates a new pipe cell. 38 | func New(r, w *os.File) cell.I { 39 | if r == nil && w == nil { 40 | var err error 41 | 42 | r, w, err = os.Pipe() 43 | if err != nil { 44 | panic(err.Error()) 45 | } 46 | } 47 | 48 | p := &pipe{ 49 | r: r, 50 | w: w, 51 | } 52 | 53 | runtime.SetFinalizer(p, (*pipe).Close) 54 | 55 | return p 56 | } 57 | 58 | // Close closes both the read and write ends of the pipe. 59 | func (p *pipe) Close() { 60 | if p.closeableReadEnd() { 61 | p.ReaderClose() 62 | } 63 | 64 | if p.closeableWriteEnd() { 65 | p.WriterClose() 66 | } 67 | } 68 | 69 | // Equal returns true if the cell c is the same pipe and false otherwise. 70 | func (p *pipe) Equal(c cell.I) bool { 71 | return Is(c) && p == To(c) 72 | } 73 | 74 | // Name returns the name of the pipe type. 75 | func (p *pipe) Name() string { 76 | return name 77 | } 78 | 79 | // Read reads a cell from the pipe. 80 | func (p *pipe) Read() cell.I { 81 | b := p.buffer() 82 | if b == nil { 83 | return pair.Null 84 | } 85 | 86 | r := p.reader() 87 | if r == nil { 88 | return pair.Null 89 | } 90 | 91 | p.RLock() 92 | defer p.RUnlock() 93 | 94 | var ( 95 | c cell.I 96 | err error 97 | ) 98 | 99 | s, ok := line(b) 100 | for ok { 101 | c, err = r.Scan(s) 102 | if err != nil { 103 | panic(err.Error()) 104 | } 105 | 106 | if c != nil { 107 | break 108 | } 109 | 110 | s, ok = line(b) 111 | } 112 | 113 | if c == nil { 114 | return pair.Null 115 | } 116 | 117 | return c 118 | } 119 | 120 | // ReadLine reads a line from the pipe. 121 | func (p *pipe) ReadLine() cell.I { 122 | b := p.buffer() 123 | if b == nil { 124 | return pair.Null 125 | } 126 | 127 | p.RLock() 128 | defer p.RUnlock() 129 | 130 | s, ok := line(b) 131 | if !ok { 132 | return pair.Null 133 | } 134 | 135 | return str.New(strings.TrimRight(s, "\n")) 136 | } 137 | 138 | // ReaderClose closes the read end of the pipe and sets it to nil. 139 | func (p *pipe) ReaderClose() { 140 | p.readerClosePipe() 141 | p.readerPipeNil() 142 | } 143 | 144 | // Write writes a cell to the pipe. 145 | func (p *pipe) Write(c cell.I) { 146 | // Yes, RLock. This is a write but doesn't change the pipe itself. 147 | p.RLock() 148 | defer p.RUnlock() 149 | 150 | if p.w == nil { 151 | panic("write to closed pipe") 152 | } 153 | 154 | _, err := p.w.WriteString(literal.String(c)) 155 | if err != nil { 156 | panic(err.Error()) 157 | } 158 | 159 | _, err = p.w.WriteString("\n") 160 | if err != nil { 161 | panic(err.Error()) 162 | } 163 | } 164 | 165 | // WriteLine writes the string value of a cell to the pipe. 166 | func (p *pipe) WriteLine(c cell.I) { 167 | // Yes, RLock. This is a write but doesn't change the pipe itself. 168 | p.RLock() 169 | defer p.RUnlock() 170 | 171 | if p.w == nil { 172 | panic("write to closed pipe") 173 | } 174 | 175 | _, err := p.w.WriteString(common.String(c)) 176 | if err != nil { 177 | panic(err.Error()) 178 | } 179 | 180 | _, err = p.w.WriteString("\n") 181 | if err != nil { 182 | panic(err.Error()) 183 | } 184 | } 185 | 186 | // WriterClose closes the write end of the pipe. 187 | func (p *pipe) WriterClose() { 188 | p.writerClosePipe() 189 | p.writerPipeNil() 190 | } 191 | 192 | func (p *pipe) buffer() *bufio.Reader { 193 | p.Lock() 194 | defer p.Unlock() 195 | 196 | if p.r == nil { 197 | return nil 198 | } 199 | 200 | if p.b == nil { 201 | p.b = bufio.NewReader(p.r) 202 | } 203 | 204 | return p.b 205 | } 206 | 207 | func (p *pipe) closeableReadEnd() bool { 208 | p.RLock() 209 | defer p.RUnlock() 210 | 211 | return p.r != nil && len(p.r.Name()) > 0 212 | } 213 | 214 | func (p *pipe) closeableWriteEnd() bool { 215 | p.RLock() 216 | defer p.RUnlock() 217 | 218 | return p.w != nil && len(p.w.Name()) > 0 219 | } 220 | 221 | func (p *pipe) reader() *reader.T { 222 | p.Lock() 223 | defer p.Unlock() 224 | 225 | if p.r == nil { 226 | return nil 227 | } 228 | 229 | if p.p == nil { 230 | p.p = reader.New(p.r.Name()) 231 | } 232 | 233 | return p.p 234 | } 235 | 236 | // readerClosePipe closes the read end of the pipe. 237 | func (p *pipe) readerClosePipe() { 238 | p.RLock() 239 | defer p.RUnlock() 240 | 241 | if p.p != nil { 242 | p.p.Close() 243 | } 244 | 245 | if p.r != nil { 246 | err := p.r.Close() 247 | if err != nil { 248 | panic(err.Error()) 249 | } 250 | } 251 | } 252 | 253 | // readerPipeNil sets the read end of the pipe to nil. 254 | func (p *pipe) readerPipeNil() { 255 | p.Lock() 256 | defer p.Unlock() 257 | 258 | p.b = nil 259 | p.p = nil 260 | p.r = nil 261 | } 262 | 263 | // writerClosePipe closes the write end of the pipe. 264 | func (p *pipe) writerClosePipe() { 265 | p.RLock() 266 | defer p.RUnlock() 267 | 268 | if p.w == nil { 269 | return 270 | } 271 | 272 | err := p.w.Close() 273 | if err != nil { 274 | panic(err.Error()) 275 | } 276 | } 277 | 278 | // writerPipeNil sets the write end of the pipe to nil. 279 | func (p *pipe) writerPipeNil() { 280 | p.Lock() 281 | defer p.Unlock() 282 | 283 | p.w = nil 284 | } 285 | 286 | // R converts c to a pipe and returns the read end of the pipe. 287 | func R(c cell.I) *os.File { 288 | return To(c).r 289 | } 290 | 291 | // W converts c to a pipe and returns the write end of the pipe. 292 | func W(c cell.I) *os.File { 293 | return To(c).w 294 | } 295 | 296 | // Read a line and return it, including the newline. 297 | func line(b *bufio.Reader) (string, bool) { 298 | s, err := b.ReadString('\n') 299 | 300 | if errors.Is(err, io.EOF) { 301 | if len(s) > 0 { 302 | return s, true 303 | } 304 | 305 | return "", false 306 | } 307 | 308 | if err != nil { 309 | panic(err.Error()) 310 | } 311 | 312 | return s, true 313 | } 314 | 315 | // A compiler-checked list of interfaces this type satisfies. Never called. 316 | func implements() { //nolint:deadcode,unused 317 | var t pipe 318 | 319 | // The pipe type is a cell. 320 | _ = cell.I(&t) 321 | 322 | // The pipe type is a conduit. 323 | _ = conduit.I(&t) 324 | } 325 | -------------------------------------------------------------------------------- /internal/common/type/pipe/pipe_internal_test.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package pipe 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | "github.com/michaelmacinnis/oh/internal/common/type/str" 10 | ) 11 | 12 | func TestWriteRead(t *testing.T) { 13 | p := New(nil, nil).(*pipe) 14 | 15 | sent := str.New("hello") 16 | 17 | p.Write(sent) 18 | 19 | received := pair.Car(p.Read()) 20 | 21 | if !received.Equal(sent) { 22 | t.Fail() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/common/type/status/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package status 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/status/status.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package status provides oh's numeric exit status type. 4 | package status 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/michaelmacinnis/oh/internal/common/interface/boolean" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 14 | ) 15 | 16 | const name = "status" 17 | 18 | // T (status) is oh's numeric status type. 19 | type T big.Rat 20 | 21 | type status = T 22 | 23 | // Int creates a status from the integer i. 24 | func Int(i int) cell.I { 25 | return Rat(big.NewRat(int64(i), 1)) 26 | } 27 | 28 | // New creates a new status cell from a string. 29 | func New(s string) cell.I { 30 | v := &big.Rat{} 31 | 32 | if _, ok := v.SetString(s); !ok { 33 | panic("'" + s + "' is not a valid number") 34 | } 35 | 36 | return Rat(v) 37 | } 38 | 39 | // Rat creates wraps the *big.Rat r as a num. 40 | func Rat(r *big.Rat) cell.I { 41 | return (*status)(r) 42 | } 43 | 44 | // Bool returns the boolean value of the status s. 45 | func (s *status) Bool() bool { 46 | return s.Rat().Cmp(&big.Rat{}) == 0 47 | } 48 | 49 | // Equal returns true if c is the same number as the status s. 50 | func (s *status) Equal(c cell.I) bool { 51 | return Is(c) && s.Rat().Cmp(To(c).Rat()) == 0 52 | } 53 | 54 | // Literal returns the literal representation of the status s. 55 | func (s *status) Literal() string { 56 | return "(|" + name + " " + s.String() + "|)" 57 | } 58 | 59 | // Rat returns the value of the status s as a *big.Rat. 60 | func (s *status) Rat() *big.Rat { 61 | return (*big.Rat)(s) 62 | } 63 | 64 | // Name returns the type name for the status s. 65 | func (s *status) Name() string { 66 | return name 67 | } 68 | 69 | // String returns the text of the status s. 70 | func (s *status) String() string { 71 | return s.Rat().RatString() 72 | } 73 | 74 | // A compiler-checked list of interfaces this type satisfies. Never called. 75 | func implements() { //nolint:deadcode,unused 76 | var t status 77 | 78 | // The status type has a boolean value. 79 | _ = boolean.I(&t) 80 | 81 | // The status type is a cell. 82 | _ = cell.I(&t) 83 | 84 | // The status type has a literal representation. 85 | _ = literal.I(&t) 86 | 87 | // The status type is a rational. 88 | _ = rational.I(&t) 89 | 90 | // The status type is a stringer. 91 | _ = fmt.Stringer(&t) 92 | } 93 | -------------------------------------------------------------------------------- /internal/common/type/str/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by type-common.oh. DO NOT EDIT. 2 | 3 | // Released under an MIT license. See LICENSE. 4 | package str 5 | 6 | import "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | 8 | // Is returns true if c is a *T. 9 | func Is(c cell.I) bool { 10 | _, ok := c.(*T) 11 | 12 | return ok 13 | } 14 | 15 | // To returns a *T if c is a *T; Otherwise it panics. 16 | func To(c cell.I) *T { 17 | if t, ok := c.(*T); ok { 18 | return t 19 | } 20 | 21 | panic("not a " + name) 22 | } 23 | -------------------------------------------------------------------------------- /internal/common/type/str/str.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package str provides oh's string type. 4 | package str 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/michaelmacinnis/adapted" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 12 | ) 13 | 14 | const name = "string" 15 | 16 | // T (str) wraps Go's string type. 17 | type T string 18 | 19 | type str = T 20 | 21 | // New creates a new str cell. 22 | func New(v string) cell.I { 23 | s := str(v) 24 | 25 | return &s 26 | } 27 | 28 | // Equal returns true if the cell c wraps the same string and false otherwise. 29 | func (s *str) Equal(c cell.I) bool { 30 | return Is(c) && s.String() == To(c).String() 31 | } 32 | 33 | // Literal returns the literal representation of the str s. 34 | func (s *str) Literal() string { 35 | return adapted.CanonicalString(string(*s)) 36 | } 37 | 38 | // Name returns the name of the str type. 39 | func (s *str) Name() string { 40 | return name 41 | } 42 | 43 | // String returns the text of the str s. 44 | func (s *str) String() string { 45 | return string(*s) 46 | } 47 | 48 | // A compiler-checked list of interfaces this type satisfies. Never called. 49 | func implements() { //nolint:deadcode,unused 50 | var t str 51 | 52 | // The str type is a cell. 53 | _ = cell.I(&t) 54 | 55 | // The str type has a literal representation. 56 | _ = literal.I(&t) 57 | 58 | // The str type is a stringer. 59 | _ = fmt.Stringer(&t) 60 | } 61 | -------------------------------------------------------------------------------- /internal/common/type/sym/conversion.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package sym 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | ) 8 | 9 | // Is returns true if c is a *T or *Plus. 10 | func Is(c cell.I) bool { 11 | switch c.(type) { 12 | case *T, *Plus: 13 | return true 14 | } 15 | 16 | return false 17 | } 18 | 19 | // To returns a *T if c is a *T or *Plus; Otherwise it panics. 20 | func To(c cell.I) *T { 21 | switch t := c.(type) { 22 | case *T: 23 | return t 24 | case *Plus: 25 | return t.sym 26 | } 27 | 28 | panic("not a " + name) 29 | } 30 | -------------------------------------------------------------------------------- /internal/common/type/sym/plus.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package sym 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/struct/loc" 8 | "github.com/michaelmacinnis/oh/internal/common/struct/token" 9 | ) 10 | 11 | // Plus is a symbol plus its lexical location. 12 | type Plus struct { 13 | *sym 14 | source *loc.T 15 | } 16 | 17 | // Token creates a Plus from a token.T. 18 | func Token(t *token.T) cell.I { 19 | p := symnew(t.Value()) 20 | 21 | return &Plus{p, t.Source()} 22 | } 23 | 24 | // Source returns the lexical location for a sym that has it. 25 | func (p *Plus) Source() *loc.T { 26 | return p.source 27 | } 28 | -------------------------------------------------------------------------------- /internal/common/type/sym/sym.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package sym provides oh's symbol cell type. 4 | package sym 5 | 6 | import ( 7 | "fmt" 8 | "math/big" 9 | "sync" 10 | 11 | "github.com/michaelmacinnis/adapted" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 14 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 15 | "github.com/michaelmacinnis/oh/internal/common/type/num" 16 | ) 17 | 18 | const ( 19 | name = "symbol" 20 | short = 3 21 | ) 22 | 23 | // T (sym) wraps Go's string type. Short and common strings are interned. 24 | type T string 25 | 26 | type sym = T 27 | 28 | // True is a reference to the symbol for 'true' - the canonical oh value for true. 29 | var True cell.I //nolint:gochecknoglobals 30 | 31 | // New creates a sym cell. 32 | func New(v string) cell.I { 33 | return symnew(v) 34 | } 35 | 36 | // Equal returns true if c is a sym and wraps the same string. 37 | func (s *sym) Equal(c cell.I) bool { 38 | return Is(c) && s.String() == To(c).String() 39 | } 40 | 41 | // Literal returns the literal representation of the sym s. 42 | func (s *sym) Literal() string { 43 | return repr(string(*s)) 44 | } 45 | 46 | // Name returns the type name for the sym s. 47 | func (s *sym) Name() string { 48 | return name 49 | } 50 | 51 | // Rat returns the value of the sym as a big.Rat, if possible. 52 | func (s *sym) Rat() *big.Rat { 53 | return rational.Number(num.New(s.Literal())) 54 | } 55 | 56 | // String returns the text of the sym s. 57 | func (s *sym) String() string { 58 | return string(*s) 59 | } 60 | 61 | // Cache enables (or disables) caching of all symbols. 62 | func Cache(a bool) { 63 | cachel.Lock() 64 | defer cachel.Unlock() 65 | 66 | all = a 67 | } 68 | 69 | //nolint:gochecknoglobals 70 | var ( 71 | all = false 72 | cache = map[string]*sym{} 73 | cachel = &sync.RWMutex{} 74 | ) 75 | 76 | func init() { //nolint:gochecknoinits 77 | v := "true" 78 | s := sym(v) 79 | 80 | True = &s 81 | cache[v] = &s 82 | } 83 | 84 | func meta(s string) string { 85 | return "(|" + name + " " + s + "|)" 86 | } 87 | 88 | func repr(s string) string { 89 | q := adapted.CanonicalString(s) 90 | 91 | if len(s) == 0 { 92 | return meta(q) 93 | } 94 | 95 | for _, r := range s { 96 | if r == ' ' { 97 | return meta(q) 98 | } 99 | } 100 | 101 | if q[2:len(q)-1] != s { 102 | return meta(q) 103 | } 104 | 105 | return s 106 | } 107 | 108 | func symnew(v string) *sym { 109 | p, ok, cacheable := symtry(v) 110 | if !ok { 111 | if cacheable { 112 | cachel.Lock() 113 | defer cachel.Unlock() 114 | 115 | if p, ok = cache[v]; ok { 116 | return p 117 | } 118 | } 119 | 120 | s := sym(v) 121 | p = &s 122 | 123 | if cacheable { 124 | cache[v] = p 125 | } 126 | } 127 | 128 | return p 129 | } 130 | 131 | func symtry(v string) (p *sym, ok, cacheable bool) { 132 | cachel.RLock() 133 | defer cachel.RUnlock() 134 | 135 | cacheable = all || len(v) <= short 136 | 137 | p, ok = cache[v] 138 | 139 | return 140 | } 141 | 142 | // A compiler-checked list of interfaces this type satisfies. Never called. 143 | func implements() { //nolint:deadcode,unused 144 | var t sym 145 | 146 | // The sym type is a cell. 147 | _ = cell.I(&t) 148 | 149 | // The sym type has a literal representation. 150 | _ = literal.I(&t) 151 | 152 | // The sym type is a stringer. 153 | _ = fmt.Stringer(&t) 154 | } 155 | -------------------------------------------------------------------------------- /internal/common/validate/validate.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package validate 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 9 | "github.com/michaelmacinnis/oh/internal/common/type/list" 10 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 11 | ) 12 | 13 | // Variadic checks that there are at least min to max arguments and returns 14 | // these as an array. Any remaining arguments are returned as a list. 15 | func Variadic(actual cell.I, min, max int) ([]cell.I, cell.I) { 16 | expected := make([]cell.I, 0, max) 17 | 18 | for i := 0; i < max; i++ { 19 | if actual == pair.Null { 20 | if i < min { 21 | s := Count(min, "argument", "s") 22 | panic(fmt.Sprintf("expected %s, passed %d", s, i)) 23 | } 24 | 25 | break 26 | } 27 | 28 | expected = append(expected, pair.Car(actual)) 29 | 30 | actual = pair.Cdr(actual) 31 | } 32 | 33 | return expected, actual 34 | } 35 | 36 | // Fixed returns min to max arguments as an array. 37 | func Fixed(actual cell.I, min, max int) []cell.I { 38 | expected, rest := Variadic(actual, min, max) 39 | if rest != pair.Null { 40 | s := Count(max, "argument", "s") 41 | n := int(list.Length(actual)) 42 | 43 | panic(fmt.Sprintf("expected %s, passed %d", s, n)) 44 | } 45 | 46 | return expected 47 | } 48 | 49 | // Count returns a human-readable count. 50 | func Count(n int, label, p string) string { 51 | if n == 1 { 52 | p = "" 53 | } 54 | 55 | return fmt.Sprintf("%d %s%s", n, label, p) 56 | } 57 | -------------------------------------------------------------------------------- /internal/engine/boot/boot.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package boot provides what is necessary for bootstrapping oh. 4 | package boot 5 | 6 | import _ "embed" // Blank import required by embed. 7 | 8 | //go:embed boot.oh 9 | var script string //nolint:gochecknoglobals 10 | 11 | // Script returns the boot script for oh. 12 | func Script() string { //nolint:funlen 13 | return script 14 | } 15 | -------------------------------------------------------------------------------- /internal/engine/commands/arithmetic.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "math/big" 7 | 8 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 9 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 10 | "github.com/michaelmacinnis/oh/internal/common/type/num" 11 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 12 | "github.com/michaelmacinnis/oh/internal/common/validate" 13 | ) 14 | 15 | func add(args cell.I) cell.I { 16 | sum := &big.Rat{} 17 | 18 | for args != pair.Null { 19 | sum.Add(sum, rational.Number(pair.Car(args))) 20 | 21 | args = pair.Cdr(args) 22 | } 23 | 24 | return num.Rat(sum) 25 | } 26 | 27 | func div(args cell.I) cell.I { 28 | v, args := validate.Variadic(args, 1, 1) 29 | 30 | quotient := &big.Rat{} 31 | quotient.Set(rational.Number(v[0])) 32 | 33 | for args != pair.Null { 34 | quotient.Quo(quotient, rational.Number(pair.Car(args))) 35 | 36 | args = pair.Cdr(args) 37 | } 38 | 39 | return num.Rat(quotient) 40 | } 41 | 42 | func mod(args cell.I) cell.I { 43 | v := validate.Fixed(args, 2, 2) 44 | 45 | remainder := rational.Number(v[0]) 46 | divisor := rational.Number(v[1]) 47 | 48 | if !remainder.IsInt() { 49 | panic("dividend must be an integer") 50 | } 51 | 52 | if !divisor.IsInt() { 53 | panic("divisor must be an integer") 54 | } 55 | 56 | dividend := &big.Int{} 57 | dividend.Set(remainder.Num()) 58 | 59 | dividend.Mod(dividend, divisor.Num()) 60 | 61 | remainder = &big.Rat{} 62 | remainder.SetInt(dividend) 63 | 64 | return num.Rat(remainder) 65 | } 66 | 67 | func mul(args cell.I) cell.I { 68 | v, args := validate.Variadic(args, 1, 1) 69 | 70 | product := &big.Rat{} 71 | product.Set(rational.Number(v[0])) 72 | 73 | for args != pair.Null { 74 | product.Mul(product, rational.Number(pair.Car(args))) 75 | 76 | args = pair.Cdr(args) 77 | } 78 | 79 | return num.Rat(product) 80 | } 81 | 82 | func sub(args cell.I) cell.I { 83 | v, args := validate.Variadic(args, 1, 1) 84 | 85 | difference := &big.Rat{} 86 | difference.Set(rational.Number(v[0])) 87 | 88 | for args != pair.Null { 89 | difference.Sub(difference, rational.Number(pair.Car(args))) 90 | 91 | args = pair.Cdr(args) 92 | } 93 | 94 | return num.Rat(difference) 95 | } 96 | -------------------------------------------------------------------------------- /internal/engine/commands/channel.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 8 | "github.com/michaelmacinnis/oh/internal/common/type/chn" 9 | "github.com/michaelmacinnis/oh/internal/common/type/create" 10 | "github.com/michaelmacinnis/oh/internal/common/validate" 11 | ) 12 | 13 | func isChan(args cell.I) cell.I { 14 | v := validate.Fixed(args, 1, 1) 15 | 16 | return create.Bool(chn.Is(v[0])) 17 | } 18 | 19 | func makeChan(args cell.I) cell.I { 20 | v := validate.Fixed(args, 0, 1) 21 | 22 | n := int64(0) 23 | if len(v) > 0 { 24 | n = integer.Value(v[0]) 25 | } 26 | 27 | return chn.New(n) 28 | } 29 | -------------------------------------------------------------------------------- /internal/engine/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | ) 8 | 9 | // Functions returns a mapping of names to 'methods' that do not reference self. 10 | func Functions() map[string]func(cell.I) cell.I { 11 | return map[string]func(cell.I) cell.I{ 12 | "add": add, 13 | "bool": makeBool, 14 | "chan": makeChan, 15 | "chan?": isChan, 16 | "cons": cons, 17 | "cons?": isCons, 18 | "debug": debug, 19 | "div": div, 20 | "equal?": equal, 21 | "ge?": ge, 22 | "gt?": gt, 23 | "le?": le, 24 | "lt?": lt, 25 | "match": match, 26 | "mend": mend, 27 | "mod": mod, 28 | "mul": mul, 29 | "not": not, 30 | "null?": isNull, 31 | "number": number, 32 | "number?": isNumber, 33 | "object?": isObject, 34 | "open": open, 35 | "pipe": makePipe, 36 | "pipe?": isPipe, 37 | "random": random, 38 | "rend": rend, 39 | "sprintf": sprintf, 40 | "status": makeStatus, 41 | "string": makeString, 42 | "string?": isString, 43 | "symbol": makeSymbol, 44 | "symbol?": isSymbol, 45 | "sub": sub, 46 | "temp-fifo": tempfifo, 47 | "umask": umask, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/engine/commands/conduit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 5 | "github.com/michaelmacinnis/oh/internal/common/interface/conduit" 6 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 7 | ) 8 | 9 | // ConduitMethods returns a mapping of names to methods shared by channels and pipes. 10 | func ConduitMethods() map[string]func(cell.I, cell.I) cell.I { 11 | return map[string]func(cell.I, cell.I) cell.I{ 12 | "close": close, 13 | "read": read, 14 | "read-line": readLine, 15 | "read-list": readList, 16 | "reader-close": readerClose, 17 | "write": write, 18 | "write-line": writeLine, 19 | "writer-close": writerClose, 20 | } 21 | } 22 | 23 | func close(s, _ cell.I) cell.I { 24 | conduit.To(s).Close() 25 | 26 | return pair.Null 27 | } 28 | 29 | func read(s, _ cell.I) cell.I { 30 | return pair.Car(conduit.To(s).Read()) 31 | } 32 | 33 | func readerClose(s, _ cell.I) cell.I { 34 | conduit.To(s).ReaderClose() 35 | 36 | return pair.Null 37 | } 38 | 39 | func readLine(s, _ cell.I) cell.I { 40 | return conduit.To(s).ReadLine() 41 | } 42 | 43 | func readList(s, _ cell.I) cell.I { 44 | return conduit.To(s).Read() 45 | } 46 | 47 | func write(s, args cell.I) cell.I { 48 | conduit.To(s).Write(args) 49 | 50 | return args 51 | } 52 | 53 | func writeLine(s, args cell.I) cell.I { 54 | conduit.To(s).WriteLine(args) 55 | 56 | return args 57 | } 58 | 59 | func writerClose(s, _ cell.I) cell.I { 60 | conduit.To(s).WriterClose() 61 | 62 | return pair.Null 63 | } 64 | -------------------------------------------------------------------------------- /internal/engine/commands/core.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "strings" 7 | 8 | "github.com/michaelmacinnis/adapted" 9 | "github.com/michaelmacinnis/oh/internal/common" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/boolean" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 14 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 15 | "github.com/michaelmacinnis/oh/internal/common/type/create" 16 | "github.com/michaelmacinnis/oh/internal/common/type/list" 17 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 18 | "github.com/michaelmacinnis/oh/internal/common/type/status" 19 | "github.com/michaelmacinnis/oh/internal/common/type/str" 20 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 21 | "github.com/michaelmacinnis/oh/internal/common/validate" 22 | ) 23 | 24 | func debug(args cell.I) cell.I { 25 | println(literal.String(args)) 26 | 27 | return sym.True 28 | } 29 | 30 | func isObject(args cell.I) cell.I { 31 | v := validate.Fixed(args, 1, 1) 32 | 33 | return create.Bool(scope.Is(v[0])) 34 | } 35 | 36 | func makeBool(args cell.I) cell.I { 37 | v := validate.Fixed(args, 1, 1) 38 | 39 | return create.Bool(boolean.Value(v[0])) 40 | } 41 | 42 | func makeStatus(args cell.I) cell.I { 43 | v := validate.Fixed(args, 1, 1) 44 | 45 | return status.Rat(rational.Number(v[0])) 46 | } 47 | 48 | func match(args cell.I) cell.I { 49 | v := validate.Fixed(args, 2, 2) 50 | 51 | ok, err := adapted.Match(common.String(v[0]), common.String(v[1])) 52 | if err != nil { 53 | panic(err.Error()) 54 | } 55 | 56 | return create.Bool(ok) 57 | } 58 | 59 | func mend(args cell.I) cell.I { 60 | v, rest := validate.Variadic(args, 2, 2) 61 | 62 | sep := common.String(v[0]) 63 | c := v[1] 64 | 65 | var create func(string) cell.I = sym.New 66 | if str.Is(c) { 67 | create = str.New 68 | } 69 | 70 | var joined strings.Builder 71 | 72 | joined.WriteString(common.String(c)) 73 | 74 | for rest != pair.Null { 75 | joined.WriteString(sep) 76 | 77 | c = pair.Car(rest) 78 | if str.Is(c) { 79 | create = str.New 80 | } 81 | 82 | joined.WriteString(common.String(c)) 83 | 84 | rest = pair.Cdr(rest) 85 | } 86 | 87 | return create(joined.String()) 88 | } 89 | 90 | func rend(args cell.I) cell.I { 91 | sep := pair.Car(args) 92 | s := pair.Cadr(args) 93 | 94 | create := sym.New 95 | if _, ok := s.(*str.T); ok { 96 | create = str.New 97 | } 98 | 99 | split := strings.Split(common.String(s), common.String(sep)) 100 | 101 | res := make([]cell.I, len(split)) 102 | for i, v := range split { 103 | res[i] = create(v) 104 | } 105 | 106 | return list.New(res...) 107 | } 108 | -------------------------------------------------------------------------------- /internal/engine/commands/file.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | 9 | "github.com/michaelmacinnis/adapted" 10 | "github.com/michaelmacinnis/oh/internal/common" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 13 | "github.com/michaelmacinnis/oh/internal/common/type/pipe" 14 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 15 | "github.com/michaelmacinnis/oh/internal/common/validate" 16 | ) 17 | 18 | func open(args cell.I) cell.I { 19 | mode := common.String(pair.Car(args)) 20 | path := common.String(pair.Cadr(args)) 21 | flags := 0 22 | 23 | if !strings.ContainsAny(mode, "-") { 24 | flags = os.O_CREATE 25 | } 26 | 27 | read := false 28 | if strings.ContainsAny(mode, "r") { 29 | read = true 30 | } 31 | 32 | write := false 33 | if strings.ContainsAny(mode, "w") { 34 | write = true 35 | 36 | if !strings.ContainsAny(mode, "a") { 37 | flags |= os.O_TRUNC 38 | } 39 | } 40 | 41 | if strings.ContainsAny(mode, "a") { 42 | write = true 43 | flags |= os.O_APPEND 44 | } 45 | 46 | if read == write { 47 | read = true 48 | write = true 49 | flags |= os.O_RDWR 50 | } else if write { 51 | flags |= os.O_WRONLY 52 | } 53 | 54 | f, err := os.OpenFile(path, flags, 0o666) 55 | if err != nil { 56 | panic(err.Error()) 57 | } 58 | 59 | r := f 60 | if !read { 61 | r = nil 62 | } 63 | 64 | w := f 65 | if !write { 66 | w = nil 67 | } 68 | 69 | return pipe.New(r, w) 70 | } 71 | 72 | func tempfifo(args cell.I) cell.I { 73 | validate.Fixed(args, 0, 0) 74 | 75 | name, err := adapted.TempFifo("fifo-") 76 | if err != nil { 77 | panic(err.Error()) 78 | } 79 | 80 | return sym.New(name) 81 | } 82 | -------------------------------------------------------------------------------- /internal/engine/commands/list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 5 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 6 | "github.com/michaelmacinnis/oh/internal/common/type/list" 7 | "github.com/michaelmacinnis/oh/internal/common/type/num" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | "github.com/michaelmacinnis/oh/internal/common/validate" 10 | ) 11 | 12 | // ListMethods returns a mapping of names to list methods. 13 | func ListMethods() map[string]func(cell.I, cell.I) cell.I { 14 | return map[string]func(cell.I, cell.I) cell.I{ 15 | "append": appendMethod, 16 | "extend": extend, 17 | "get": get, 18 | "head": head, 19 | "length": length, 20 | "reverse": reverse, 21 | "set-head": setHead, 22 | "set-tail": setTail, 23 | "slice": slice, 24 | "tail": tail, 25 | } 26 | } 27 | 28 | func appendMethod(s, args cell.I) cell.I { 29 | v := validate.Fixed(args, 1, 1) 30 | 31 | self := pair.To(s) 32 | 33 | return list.Append(self, v...) 34 | } 35 | 36 | func extend(s, args cell.I) cell.I { 37 | v := validate.Fixed(args, 1, 1) 38 | 39 | self := pair.To(s) 40 | 41 | return list.Join(self, v[0]) 42 | } 43 | 44 | func get(s, args cell.I) cell.I { 45 | v, args := validate.Variadic(args, 0, 1) 46 | 47 | self := pair.To(s) 48 | 49 | i := int64(0) 50 | if len(v) != 0 { 51 | i = integer.Value(v[0]) 52 | } 53 | 54 | var dflt cell.I 55 | if args != pair.Null { 56 | dflt = args 57 | } 58 | 59 | return pair.Car(list.Tail(self, i, dflt)) 60 | } 61 | 62 | func head(s, _ cell.I) cell.I { 63 | return pair.Car(pair.To(s)) 64 | } 65 | 66 | func length(s, args cell.I) cell.I { 67 | validate.Fixed(args, 0, 0) 68 | 69 | return num.Int(int(list.Length(pair.To(s)))) 70 | } 71 | 72 | func reverse(s, args cell.I) cell.I { 73 | validate.Fixed(args, 0, 0) 74 | 75 | return list.Reverse(pair.To(s)) 76 | } 77 | 78 | func setHead(s, args cell.I) cell.I { 79 | v := pair.Car(args) 80 | pair.SetCar(s, v) 81 | 82 | return v 83 | } 84 | 85 | func setTail(s, args cell.I) cell.I { 86 | v := pair.Car(args) 87 | pair.SetCdr(s, v) 88 | 89 | return v 90 | } 91 | 92 | func slice(s, args cell.I) cell.I { 93 | v := validate.Fixed(args, 1, 2) 94 | 95 | start := integer.Value(v[0]) 96 | end := int64(0) 97 | 98 | if len(v) == 2 { //nolint:gomnd 99 | end = integer.Value(v[1]) 100 | } 101 | 102 | return list.Slice(s, start, end) 103 | } 104 | 105 | func tail(s, _ cell.I) cell.I { 106 | return pair.Cdr(pair.To(s)) 107 | } 108 | -------------------------------------------------------------------------------- /internal/engine/commands/logical.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/boolean" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/type/create" 9 | "github.com/michaelmacinnis/oh/internal/common/validate" 10 | ) 11 | 12 | func not(args cell.I) cell.I { 13 | v := validate.Fixed(args, 1, 1) 14 | 15 | return create.Bool(!boolean.Value(v[0])) 16 | } 17 | -------------------------------------------------------------------------------- /internal/engine/commands/number.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "math" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/michaelmacinnis/oh/internal/common" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 12 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 14 | "github.com/michaelmacinnis/oh/internal/common/type/create" 15 | "github.com/michaelmacinnis/oh/internal/common/type/num" 16 | "github.com/michaelmacinnis/oh/internal/common/validate" 17 | ) 18 | 19 | func init() { //nolint:gochecknoinits 20 | rand.Seed(time.Now().UnixNano()) 21 | } 22 | 23 | func isNumber(args cell.I) cell.I { 24 | v := validate.Fixed(args, 1, 1) 25 | 26 | return create.Bool(num.Is(v[0])) 27 | } 28 | 29 | func number(args cell.I) cell.I { 30 | v := validate.Fixed(args, 1, 1) 31 | 32 | if r, ok := v[0].(rational.I); ok { 33 | return num.Rat(r.Rat()) 34 | } 35 | 36 | return num.New(common.String(v[0])) 37 | } 38 | 39 | func random(args cell.I) cell.I { 40 | v := validate.Fixed(args, 0, 1) 41 | 42 | n := math.MaxInt32 43 | if len(v) == 1 { 44 | n = int(integer.Value(v[0])) 45 | } 46 | 47 | return num.Int(rand.Intn(n)) //nolint:gosec 48 | } 49 | -------------------------------------------------------------------------------- /internal/engine/commands/pair.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/type/create" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | "github.com/michaelmacinnis/oh/internal/common/validate" 10 | ) 11 | 12 | func cons(args cell.I) cell.I { 13 | v := validate.Fixed(args, 2, 2) 14 | 15 | return pair.Cons(v[0], v[1]) 16 | } 17 | 18 | func isCons(args cell.I) cell.I { 19 | v := validate.Fixed(args, 1, 1) 20 | 21 | return create.Bool(pair.Is(v[0])) 22 | } 23 | 24 | func isNull(args cell.I) cell.I { 25 | v := validate.Fixed(args, 1, 1) 26 | 27 | return create.Bool(v[0] == pair.Null) 28 | } 29 | -------------------------------------------------------------------------------- /internal/engine/commands/pipe.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/type/create" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pipe" 9 | "github.com/michaelmacinnis/oh/internal/common/validate" 10 | ) 11 | 12 | func isPipe(args cell.I) cell.I { 13 | v := validate.Fixed(args, 1, 1) 14 | 15 | return create.Bool(pipe.Is(v[0])) 16 | } 17 | 18 | func makePipe(args cell.I) cell.I { 19 | validate.Fixed(args, 0, 0) 20 | 21 | return pipe.New(nil, nil) 22 | } 23 | -------------------------------------------------------------------------------- /internal/engine/commands/relational.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/rational" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 10 | "github.com/michaelmacinnis/oh/internal/common/validate" 11 | ) 12 | 13 | func equal(args cell.I) cell.I { 14 | v, rest := validate.Variadic(args, 2, 2) 15 | 16 | for { 17 | if !v[0].Equal(v[1]) { 18 | return pair.Null 19 | } 20 | 21 | if rest == pair.Null { 22 | return sym.True 23 | } 24 | 25 | v[0] = v[1] 26 | v[1] = pair.Car(rest) 27 | 28 | rest = pair.Cdr(rest) 29 | } 30 | } 31 | 32 | func ge(args cell.I) cell.I { 33 | v, rest := validate.Variadic(args, 2, 2) 34 | 35 | prev := rational.Number(v[0]) 36 | curr := rational.Number(v[1]) 37 | 38 | for { 39 | if prev.Cmp(curr) < 0 { 40 | return pair.Null 41 | } 42 | 43 | if rest == pair.Null { 44 | return sym.True 45 | } 46 | 47 | prev = curr 48 | curr = rational.Number(pair.Car(rest)) 49 | 50 | rest = pair.Cdr(rest) 51 | } 52 | } 53 | 54 | func gt(args cell.I) cell.I { 55 | v, rest := validate.Variadic(args, 2, 2) 56 | 57 | prev := rational.Number(v[0]) 58 | curr := rational.Number(v[1]) 59 | 60 | for { 61 | if prev.Cmp(curr) <= 0 { 62 | return pair.Null 63 | } 64 | 65 | if rest == pair.Null { 66 | return sym.True 67 | } 68 | 69 | prev = curr 70 | curr = rational.Number(pair.Car(rest)) 71 | 72 | rest = pair.Cdr(rest) 73 | } 74 | } 75 | 76 | func le(args cell.I) cell.I { 77 | v, rest := validate.Variadic(args, 2, 2) 78 | 79 | prev := rational.Number(v[0]) 80 | curr := rational.Number(v[1]) 81 | 82 | for { 83 | if prev.Cmp(curr) > 0 { 84 | return pair.Null 85 | } 86 | 87 | if rest == pair.Null { 88 | return sym.True 89 | } 90 | 91 | prev = curr 92 | curr = rational.Number(pair.Car(rest)) 93 | 94 | rest = pair.Cdr(rest) 95 | } 96 | } 97 | 98 | func lt(args cell.I) cell.I { 99 | v, rest := validate.Variadic(args, 2, 2) 100 | 101 | prev := rational.Number(v[0]) 102 | curr := rational.Number(v[1]) 103 | 104 | for { 105 | if prev.Cmp(curr) >= 0 { 106 | return pair.Null 107 | } 108 | 109 | if rest == pair.Null { 110 | return sym.True 111 | } 112 | 113 | prev = curr 114 | curr = rational.Number(pair.Car(rest)) 115 | 116 | rest = pair.Cdr(rest) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/engine/commands/string.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 11 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 12 | "github.com/michaelmacinnis/oh/internal/common/type/create" 13 | "github.com/michaelmacinnis/oh/internal/common/type/num" 14 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 15 | "github.com/michaelmacinnis/oh/internal/common/type/str" 16 | "github.com/michaelmacinnis/oh/internal/common/validate" 17 | ) 18 | 19 | // StringFunctions returns a mapping of names to string methods. 20 | func StringFunctions() map[string]func(cell.I) cell.I { 21 | return map[string]func(cell.I) cell.I{ 22 | "format": sprintf, 23 | "length": slength, 24 | "lower": lower, 25 | "replace": sreplace, 26 | "slice": sslice, 27 | "trim-prefix": trimPrefix, 28 | "trim-suffix": trimSuffix, 29 | "upper": upper, 30 | } 31 | } 32 | 33 | func isString(args cell.I) cell.I { 34 | v := validate.Fixed(args, 1, 1) 35 | 36 | return create.Bool(str.Is(v[0])) 37 | } 38 | 39 | func lower(args cell.I) cell.I { 40 | v := validate.Fixed(args, 1, 1) 41 | 42 | return str.New(strings.ToLower(common.String(v[0]))) 43 | } 44 | 45 | func makeString(args cell.I) cell.I { 46 | v := validate.Fixed(args, 1, 1) 47 | 48 | return str.New(common.String(v[0])) 49 | } 50 | 51 | func slength(args cell.I) cell.I { 52 | v := validate.Fixed(args, 1, 1) 53 | 54 | return num.Int(len(common.String(v[0]))) 55 | } 56 | 57 | func sslice(args cell.I) cell.I { 58 | v := validate.Fixed(args, 2, 3) 59 | 60 | s := common.String(v[0]) 61 | 62 | length := int64(len(s)) 63 | 64 | start := integer.Value(v[1]) 65 | if start < 0 { 66 | panic("slice starts before first element") 67 | } else if start > length { 68 | start = length 69 | } 70 | 71 | end := length 72 | if len(v) == 3 { //nolint:gomnd 73 | end = integer.Value(v[2]) 74 | if end > length { 75 | end = length 76 | } else if end < 0 { 77 | end = length + end 78 | } 79 | } 80 | 81 | if end < start { 82 | panic("end of slice before start") 83 | } 84 | 85 | return str.New(s[start:end]) 86 | } 87 | 88 | func sreplace(args cell.I) cell.I { 89 | v := validate.Fixed(args, 3, 4) 90 | 91 | s := common.String(v[0]) 92 | old := common.String(v[1]) 93 | replacement := common.String(v[2]) 94 | 95 | n := -1 96 | // The 4th argument, if passed, limits the number of replacements. 97 | if len(v) == 4 { //nolint:gomnd 98 | n = int(integer.Value(v[3])) 99 | } 100 | 101 | return str.New(strings.Replace(s, old, replacement, n)) 102 | } 103 | 104 | // TODO: Extend oh types to play nicer with fmt and Sprintf. 105 | func sprintf(args cell.I) cell.I { 106 | v, args := validate.Variadic(args, 1, 1) 107 | 108 | argv := []interface{}{} 109 | 110 | for args != pair.Null { 111 | argv = append(argv, pair.Car(args)) 112 | args = pair.Cdr(args) 113 | } 114 | 115 | return str.New(fmt.Sprintf(common.String(v[0]), argv...)) 116 | } 117 | 118 | func trimPrefix(args cell.I) cell.I { 119 | v := validate.Fixed(args, 2, 2) 120 | 121 | return str.New(strings.TrimPrefix(common.String(v[0]), common.String(v[1]))) 122 | } 123 | 124 | func trimSuffix(args cell.I) cell.I { 125 | v := validate.Fixed(args, 2, 2) 126 | 127 | return str.New(strings.TrimSuffix(common.String(v[0]), common.String(v[1]))) 128 | } 129 | 130 | func upper(args cell.I) cell.I { 131 | v := validate.Fixed(args, 1, 1) 132 | 133 | return str.New(strings.ToUpper(common.String(v[0]))) 134 | } 135 | -------------------------------------------------------------------------------- /internal/engine/commands/symbol.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 8 | "github.com/michaelmacinnis/oh/internal/common/type/create" 9 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 10 | "github.com/michaelmacinnis/oh/internal/common/validate" 11 | ) 12 | 13 | func isSymbol(args cell.I) cell.I { 14 | v := validate.Fixed(args, 1, 1) 15 | 16 | return create.Bool(sym.Is(v[0])) 17 | } 18 | 19 | func makeSymbol(args cell.I) cell.I { 20 | v := validate.Fixed(args, 1, 1) 21 | 22 | return sym.New(common.String(v[0])) 23 | } 24 | -------------------------------------------------------------------------------- /internal/engine/commands/umask_unix.go: -------------------------------------------------------------------------------- 1 | // Use of this source code is governed by a BSD-style 2 | // license that can be found in the LICENSE file. 3 | 4 | package commands 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 10 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 11 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 12 | "github.com/michaelmacinnis/oh/internal/common/validate" 13 | "github.com/michaelmacinnis/oh/internal/system/process" 14 | ) 15 | 16 | func umask(args cell.I) cell.I { 17 | v := validate.Fixed(args, 0, 1) 18 | 19 | nmask := int64(0) 20 | if len(v) == 1 { 21 | nmask = integer.Value(v[0]) 22 | } 23 | 24 | omask := process.Umask(int(nmask)) 25 | 26 | if nmask == 0 { 27 | process.Umask(omask) 28 | } 29 | 30 | return sym.New(fmt.Sprintf("0o%o", omask)) 31 | } 32 | -------------------------------------------------------------------------------- /internal/engine/engine.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package engine provides an evaluator for parsed oh code. 4 | package engine 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/michaelmacinnis/oh/internal/common" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/boolean" 14 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 15 | "github.com/michaelmacinnis/oh/internal/common/interface/integer" 16 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 17 | "github.com/michaelmacinnis/oh/internal/common/struct/frame" 18 | "github.com/michaelmacinnis/oh/internal/common/type/env" 19 | "github.com/michaelmacinnis/oh/internal/common/type/list" 20 | "github.com/michaelmacinnis/oh/internal/common/type/num" 21 | "github.com/michaelmacinnis/oh/internal/common/type/obj" 22 | "github.com/michaelmacinnis/oh/internal/common/type/pipe" 23 | "github.com/michaelmacinnis/oh/internal/common/type/status" 24 | "github.com/michaelmacinnis/oh/internal/common/type/str" 25 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 26 | "github.com/michaelmacinnis/oh/internal/common/validate" 27 | "github.com/michaelmacinnis/oh/internal/engine/boot" 28 | "github.com/michaelmacinnis/oh/internal/engine/task" 29 | "github.com/michaelmacinnis/oh/internal/reader" 30 | "github.com/michaelmacinnis/oh/internal/system/job" 31 | "github.com/michaelmacinnis/oh/internal/system/process" 32 | ) 33 | 34 | // Boot gets things ready for Evaluate and System calls. 35 | func Boot(path string, arguments []string) { 36 | sym.Cache(true) 37 | 38 | job.Monitor() 39 | 40 | if path != "" { 41 | path = filepath.Dir(path) 42 | } 43 | 44 | path, err := filepath.Abs(path) 45 | if err != nil { 46 | panic(err.Error()) 47 | } 48 | 49 | env0.Export("ORIGIN", sym.New(path)) 50 | 51 | pwd, err := os.Getwd() 52 | if err != nil { 53 | panic(err.Error()) 54 | } 55 | 56 | env0.Export("OLDPWD", sym.New(pwd)) 57 | env0.Export("PWD", sym.New(pwd)) 58 | 59 | if len(arguments) > 0 { 60 | args := make([]cell.I, 0, len(arguments)) 61 | 62 | for n, s := range arguments { 63 | v := str.New(s) 64 | args = append(args, v) 65 | env0.Export(strconv.Itoa(n), v) 66 | } 67 | 68 | env0.Export("@", list.New(args[1:]...)) 69 | } 70 | 71 | j := job.Job(0) 72 | r := reader.New("boot.oh") 73 | 74 | for _, line := range strings.SplitAfter(boot.Script(), "\n") { 75 | c, err := r.Scan(line) 76 | if err != nil { 77 | panic(err.Error()) 78 | } 79 | 80 | if c != nil { 81 | System(j, c) 82 | } 83 | } 84 | 85 | sym.Cache(false) 86 | } 87 | 88 | // Evaluate evaluates the command c. 89 | func Evaluate(j *job.T, c cell.I) cell.I { 90 | r, exited := System(j, c) 91 | 92 | if exited { 93 | code, ok := exitcode(r) 94 | if !ok { 95 | code = success(r) 96 | } 97 | 98 | os.Exit(code) 99 | } 100 | 101 | scope0.Define("?", r) 102 | 103 | return r 104 | } 105 | 106 | // Resolve returns the string value for a variable. 107 | func Resolve(k string) (v string) { 108 | defer func() { 109 | r := recover() 110 | if r != nil { 111 | v = "" 112 | } 113 | }() 114 | 115 | _, r := frame0.Resolve(k) 116 | if r == nil { 117 | return 118 | } 119 | 120 | v = common.String(r.Get()) 121 | 122 | return 123 | } 124 | 125 | // System evaluates the command c returning the result and if the task exited. 126 | func System(j *job.T, c cell.I) (cell.I, bool) { 127 | t := task.New(j, c, frame0) 128 | 129 | t.PushOp(task.Action(task.EvalCommand)) 130 | 131 | done := make(chan struct{}) 132 | 133 | j.Spawn(nil, t, func() { 134 | close(done) 135 | }) 136 | 137 | <-done 138 | 139 | return t.Result(), t.Exited() 140 | } 141 | 142 | //nolint:gochecknoglobals 143 | var ( 144 | env0 scope.I 145 | frame0 *frame.T 146 | scope0 scope.I 147 | ) 148 | 149 | func bg(t *task.T) task.Op { 150 | v := validate.Fixed(t.Code(), 0, 1) 151 | 152 | n := 0 153 | if len(v) > 0 { 154 | n = int(integer.Value(v[0])) 155 | } 156 | 157 | // TODO: Convert this to a function that returns what a wrapper needs. 158 | bt := job.Bg(pipe.W(t.CellValue("stdout")), n) 159 | if bt == nil { 160 | panic("job does not exist") 161 | } 162 | 163 | return t.Return(bt) 164 | } 165 | 166 | func exitcode(c cell.I) (code int, ok bool) { 167 | defer func() { 168 | r := recover() 169 | if r != nil { 170 | ok = false 171 | } 172 | }() 173 | 174 | return int(integer.Value(c)), true 175 | } 176 | 177 | func fg(t *task.T) task.Op { 178 | v := validate.Fixed(t.Code(), 0, 1) 179 | 180 | n := 0 181 | if len(v) > 0 { 182 | n = int(integer.Value(v[0])) 183 | } 184 | 185 | // TODO: Convert this to a function that returns what a wrapper needs. 186 | if !job.Fg(pipe.W(t.CellValue("stdout")), n) { 187 | panic("job does not exist") 188 | } 189 | 190 | return t.Return(sym.True) 191 | } 192 | 193 | func init() { //nolint:gochecknoinits 194 | ee := &task.Syntax{Op: task.Action(task.EvalExport)} 195 | 196 | env0 = env.New(nil) 197 | 198 | env0.Export("export", ee) 199 | 200 | env0.Export("stdin", pipe.New(os.Stdin, nil)) 201 | env0.Export("stdout", pipe.New(nil, os.Stdout)) 202 | env0.Export("stderr", pipe.New(nil, os.Stderr)) 203 | 204 | // Environment variables. 205 | for _, entry := range os.Environ() { 206 | kv := strings.SplitN(entry, "=", 2) 207 | env0.Export(kv[0], sym.New(kv[1])) 208 | } 209 | 210 | scope0 = env.New(nil) 211 | 212 | scope0.Export("export", ee) 213 | 214 | scope0.Define("$", num.Int(process.ID())) 215 | 216 | scope0.Define("str", task.StringScope()) 217 | scope0.Define("sys", obj.New(env0)) 218 | 219 | // Methods. 220 | scope0.Define("bg", &task.Method{Op: task.Action(bg)}) 221 | scope0.Define("fg", &task.Method{Op: task.Action(fg)}) 222 | scope0.Define("jobs", &task.Method{Op: task.Action(jobs)}) 223 | 224 | task.Actions(scope0) 225 | 226 | frame0 = frame.New(scope0, frame.New(env0, nil)) 227 | } 228 | 229 | func jobs(t *task.T) task.Op { 230 | // TODO: Convert this to a function that returns what a wrapper needs. 231 | job.Jobs(pipe.W(t.CellValue("stdout"))) 232 | 233 | return t.Return(status.Int(0)) 234 | } 235 | 236 | func success(c cell.I) (exitcode int) { 237 | defer func() { 238 | recover() //nolint:errcheck 239 | }() 240 | 241 | return map[bool]int{ 242 | true: 0, 243 | false: 1, 244 | }[boolean.Value(c)] 245 | } 246 | -------------------------------------------------------------------------------- /internal/engine/task/closure.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 8 | ) 9 | 10 | // Closure underlies the method, and syntax types. 11 | type Closure struct { 12 | Body cell.I // Body of the routine. 13 | Labels 14 | Op 15 | Scope scope.I 16 | } 17 | 18 | // Labels hold the labels for a user-defined routine. 19 | type Labels struct { 20 | Env cell.I // Calling env label. 21 | Params cell.I // Param labels. 22 | Self cell.I // Label for the env where this routine was found. 23 | } 24 | -------------------------------------------------------------------------------- /internal/engine/task/method.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | ) 8 | 9 | // Method is oh's arguments-evaluated, closure type. 10 | type Method Closure 11 | 12 | // The method type is a cell. 13 | 14 | // Equal returns true if the cell c is the same method as a. 15 | func (a *Method) Equal(c cell.I) bool { 16 | p, ok := c.(*Method) 17 | 18 | return ok && p == a 19 | } 20 | 21 | // Name returns the name of the method type. 22 | func (a *Method) Name() string { 23 | return "method" 24 | } 25 | 26 | // Methods specific to method. 27 | 28 | // Closure returns the method a's underlying closure. 29 | func (a *Method) Closure() *Closure { 30 | return (*Closure)(a) 31 | } 32 | 33 | // Execute sets up the operations required to execute the method a. 34 | func (a *Method) Execute(t *T) Op { 35 | t.ReplaceOp(a.Op) 36 | t.PushOp(Action(execMethod)) 37 | t.PushResult(nil) 38 | 39 | return t.PushOp(Action(evalArgs)) 40 | } 41 | 42 | var _ command = &Method{} 43 | -------------------------------------------------------------------------------- /internal/engine/task/operation.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "reflect" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | // Op represents a single step of a task. 12 | type Op interface { 13 | Perform(*T) Op 14 | } 15 | 16 | func opString(o Op) string { 17 | if o == nil { 18 | return "" 19 | } 20 | 21 | if a, ok := o.(Action); ok { 22 | return funcName(a) 23 | } 24 | 25 | if r, ok := o.(*registers); ok { 26 | s := "Restore(" 27 | comma := "" 28 | 29 | if r.code != nil { 30 | s += "code" 31 | comma = ", " 32 | } 33 | 34 | if r.dump != nil { 35 | s += comma + "dump" 36 | comma = ", " 37 | } 38 | 39 | if r.frame != nil { 40 | s += comma + "frame" 41 | comma = ", " 42 | } 43 | 44 | if r.stack != nil { 45 | s += comma + "dump" 46 | } 47 | 48 | s += ")" 49 | 50 | return s 51 | } 52 | 53 | return "" 54 | } 55 | 56 | // Get the function i's name. Useful for debugging. 57 | func funcName(i interface{}) string { 58 | n := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 59 | 60 | a := strings.Split(n, ".") 61 | 62 | l := len(a) 63 | if l == 0 { 64 | return n 65 | } 66 | 67 | return a[l-1] 68 | } 69 | -------------------------------------------------------------------------------- /internal/engine/task/registers.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/struct/frame" 8 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 9 | ) 10 | 11 | // The registers type holds the state of oh's stack-based abstract machine. 12 | type registers struct { 13 | *stack 14 | frame *frame.T 15 | code cell.I 16 | dump cell.I 17 | } 18 | 19 | // Perform copies non-nil fields from m to target. 20 | func (m *registers) Perform(target *T) Op { 21 | m.restoreOver(target.registers) 22 | 23 | return target.PreviousOp() 24 | } 25 | 26 | func (m *registers) Code() cell.I { 27 | return m.code 28 | } 29 | 30 | func (m *registers) Completed() bool { 31 | return m.stack == done 32 | } 33 | 34 | func (m *registers) Equal(c cell.I) bool { 35 | switch c := c.(type) { 36 | case *registers: 37 | return *m == *c 38 | default: 39 | return false 40 | } 41 | } 42 | 43 | func (m *registers) Name() string { 44 | return "continuation" 45 | } 46 | 47 | // Op returns the abstract machine's current operation. 48 | func (m *registers) Op() Op { 49 | return m.stack.op 50 | } 51 | 52 | // PopResult removes the top result from dump. 53 | func (m *registers) PopResult() cell.I { 54 | // println("pop result") 55 | r := pair.Car(m.dump) 56 | m.dump = pair.Cdr(m.dump) 57 | 58 | return r 59 | } 60 | 61 | // PushOp pushes a new operation onto the stack. 62 | func (m *registers) PushOp(s Op) Op { 63 | /* 64 | if a, ok := s.(Action); ok { 65 | println("PushOp(" + funcName(a) + ")") 66 | } else { 67 | println("PushOp(SaveOp)") 68 | } 69 | */ 70 | current := toRegisters(s) 71 | previous := toRegisters(m.stack.op) 72 | 73 | if current != nil && previous != nil { 74 | // Condense restore operations. 75 | previous.restoreOver(current) 76 | m.stack.op = current 77 | } else { 78 | m.stack = &stack{m.stack, s} 79 | } 80 | 81 | return s 82 | } 83 | 84 | // PushResult adds the result r to dump. 85 | func (m *registers) PushResult(r cell.I) { 86 | // println("push result") 87 | m.dump = pair.Cons(r, m.dump) 88 | } 89 | 90 | // PreviousOp pops the current operation and returns the previous operation. 91 | func (m *registers) PreviousOp() Op { 92 | m.RemoveOp() 93 | 94 | return m.Op() 95 | } 96 | 97 | // RemoveOp pops the current operation off the stack. 98 | func (m *registers) RemoveOp() { 99 | /* 100 | if a, ok := m.stack.op.(Action); ok { 101 | println("RemoveOp("+funcName(a)+")") 102 | } else { 103 | println("RemoveOp(SaveOp)") 104 | } 105 | */ 106 | m.stack = m.stack.stack 107 | } 108 | 109 | // ReplaceOp replaces the operation at the top of the stack. 110 | func (m *registers) ReplaceOp(s Op) Op { 111 | m.RemoveOp() 112 | 113 | return m.PushOp(s) 114 | } 115 | 116 | // ReplaceResult replaced the current result. 117 | func (m *registers) ReplaceResult(r cell.I) { 118 | // println("replace result") 119 | m.dump = pair.Cons(r, pair.Cdr(m.dump)) 120 | } 121 | 122 | // Result returns the current result. 123 | func (m *registers) Result() cell.I { 124 | return pair.Car(m.dump) 125 | } 126 | 127 | // The stack type is a machine's execution stack. 128 | type stack struct { 129 | *stack 130 | op Op 131 | } 132 | 133 | //nolint:gochecknoglobals 134 | var ( 135 | done = &stack{} 136 | ) 137 | 138 | func (m *registers) arguments() cell.I { 139 | e := m.PopResult() 140 | l := pair.Null 141 | 142 | for e != nil && m.dump != pair.Null { 143 | l = pair.Cons(e, l) 144 | 145 | e = m.PopResult() 146 | } 147 | 148 | return l 149 | } 150 | 151 | func (m *registers) restoreOver(target *registers) { 152 | if m.frame != nil { 153 | target.frame = m.frame 154 | } 155 | 156 | if m.code != nil { 157 | target.code = m.code 158 | } 159 | 160 | if m.dump != nil { 161 | target.dump = m.dump 162 | } 163 | 164 | if m.stack != nil { 165 | target.stack = m.stack 166 | } 167 | } 168 | 169 | func init() { //nolint:gochecknoinits 170 | done.stack = done 171 | } 172 | 173 | func toRegisters(s Op) *registers { 174 | if r, ok := s.(*registers); ok { 175 | return r 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /internal/engine/task/state.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 9 | ) 10 | 11 | // E R S W 12 | // 0 1 0 X Task is running. 13 | // 0 1 1 X Task is stopping but Runnable has not yet been called. 14 | // 0 0 1 X Task is stopping but has not returned from Run. 15 | // 0 0 0 X Task is stopped. 16 | 17 | // E R S W 18 | // X X X 0 Task is not waiting. 19 | // X X X 1 Task is waiting for an external process to finish. 20 | 21 | // E R S W 22 | // 1 X X X Task is no longer runnable. 23 | 24 | // The type state is a task's state. 25 | type state struct { 26 | *sync.Mutex 27 | resume *sync.Cond 28 | stopped *sync.Cond 29 | 30 | result cell.I 31 | 32 | exited bool 33 | running bool 34 | stopping bool 35 | waiting bool 36 | } 37 | 38 | func fresh() *state { 39 | m := &sync.Mutex{} 40 | 41 | return &state{Mutex: m, resume: sync.NewCond(m), stopped: sync.NewCond(m)} 42 | } 43 | 44 | func (s *state) Exit() { 45 | s.Lock() 46 | defer s.Unlock() 47 | 48 | s.exited = true 49 | } 50 | 51 | func (s *state) Exited() bool { 52 | s.Lock() 53 | defer s.Unlock() 54 | 55 | return s.exited 56 | } 57 | 58 | // Notify notifies a waiting task that it can continue. If running is set, 59 | // the condition variable resume is used to signal the task to resume. 60 | // Calling this on a task that is not waiting results in a panic. 61 | // 62 | // R S W -> R S W 63 | // X X 0 X X 0 panic 64 | // 0 X 1 0 X 0 65 | // 1 X 1 1 X 0 resume signal. 66 | // 67 | func (s *state) Notify(r cell.I) { 68 | s.Lock() 69 | defer s.Unlock() 70 | 71 | if !s.waiting { 72 | panic("can't resume a task that isn't waiting.") 73 | } 74 | 75 | s.result = r 76 | s.waiting = false 77 | 78 | if s.running { 79 | s.resume.Signal() 80 | } 81 | } 82 | 83 | // Runnable returns true if a task is running and not stopping or waiting. 84 | // If the task is waiting and not stopping Runnable blocks until signaled via 85 | // the resume condition variable. 86 | // 87 | // R S W -> R S W 88 | // 0 X X 0 X X returns false 89 | // 1 0 0 1 0 0 returns true 90 | // 1 0 1 0 1 1 blocks until stopped, ... 91 | // 1 0 1 1 0 0 ...or resumed 92 | // 1 1 0 0 1 0 returns false 93 | // 1 1 1 0 1 1 . 94 | // 95 | func (s *state) Runnable() bool { 96 | s.Lock() 97 | defer s.Unlock() 98 | 99 | if !s.running { 100 | return false 101 | } 102 | 103 | for s.waiting && !s.stopping { 104 | s.resume.Wait() 105 | } 106 | 107 | s.running = !s.stopping 108 | 109 | return s.running 110 | } 111 | 112 | // Started marks the task as running. 113 | // Calling this on a task that is already running results in a panic. 114 | // 115 | // R S W -> R S W 116 | // 0 0 X -> 1 0 X 117 | // 0 1 X -> 0 0 X 118 | // 1 X X 1 X X panic. 119 | // 120 | func (s *state) Started() { 121 | s.Lock() 122 | defer s.Unlock() 123 | 124 | if s.running { 125 | // Why is this already running? 126 | panic("already running") 127 | } 128 | 129 | s.running = true 130 | } 131 | 132 | // Stop marks a running task as stopping and waits for a signal that the task 133 | // has stopped. If the task is waiting Stop signals it to resume so that it 134 | // can unblock and signal that it has stopped. 135 | // 136 | // R S W -> R S W 137 | // X 1 X -> X 1 X 138 | // 0 X X -> 0 X X 139 | // 1 0 0 -> 0 0 0 waits for stopped signal 140 | // 1 0 1 -> 0 0 1 resumes tasks, waits for stopped signal. 141 | // 142 | func (s *state) Stop(f func()) { 143 | s.Lock() 144 | defer s.Unlock() 145 | 146 | if s.stopping { 147 | return 148 | } 149 | 150 | if !s.running { 151 | // Stopped running before we got here. 152 | return 153 | } 154 | 155 | s.stopping = true 156 | 157 | if f != nil { 158 | f() 159 | } 160 | 161 | if s.waiting { 162 | s.resume.Signal() 163 | } 164 | 165 | for !s.stopping { 166 | s.stopped.Wait() 167 | } 168 | } 169 | 170 | // Stopped clears running and stopping and if stopping was set, signals that 171 | // the task has stopped. 172 | // 173 | // R S W -> R S W 174 | // X 0 X -> 0 0 X 175 | // 0 1 X -> 0 0 X signals task has stopped. 176 | // 177 | func (s *state) Stopped() { 178 | s.Lock() 179 | defer s.Unlock() 180 | 181 | signal := s.stopping 182 | 183 | s.running = false 184 | s.stopping = false 185 | 186 | if signal { 187 | s.stopped.Signal() 188 | } 189 | } 190 | 191 | // Value returns the most recent value set by notify. 192 | func (s *state) Value() cell.I { 193 | s.Lock() 194 | defer s.Unlock() 195 | 196 | r := s.result 197 | s.result = nil 198 | 199 | return r 200 | } 201 | 202 | // Wait sets waiting. Calling this when waiting is already set panics. 203 | // 204 | // R S W -> R S W 205 | // X X 0 -> X X 1 206 | // X X 1 -> X X 1 panic. 207 | // 208 | func (s *state) Wait() { 209 | s.Lock() 210 | defer s.Unlock() 211 | 212 | if s.waiting { 213 | // Why is this already waiting? 214 | panic("already waiting") 215 | } 216 | 217 | s.waiting = true 218 | } 219 | -------------------------------------------------------------------------------- /internal/engine/task/syntax.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | package task 4 | 5 | import ( 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | ) 8 | 9 | // Syntax is oh's arguments-not-evaluated, closure type. 10 | type Syntax Closure 11 | 12 | // The syntax type is a cell. 13 | 14 | // Equal returns true if the cell c is the same syntax as a. 15 | func (a *Syntax) Equal(c cell.I) bool { 16 | p, ok := c.(*Syntax) 17 | 18 | return ok && p == a 19 | } 20 | 21 | // Name returns the name of the syntax type. 22 | func (a *Syntax) Name() string { 23 | return "syntax" 24 | } 25 | 26 | // Methods specific to syntax. 27 | 28 | // Closure returns the syntax a's underlying closure. 29 | func (a *Syntax) Closure() *Closure { 30 | return (*Closure)(a) 31 | } 32 | 33 | // Execute sets up the operations required to execute the syntax a. 34 | func (a *Syntax) Execute(t *T) Op { 35 | return t.ReplaceOp(a.Op) 36 | } 37 | -------------------------------------------------------------------------------- /internal/engine/task/task.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package task provides the machinery used by oh tasks. 4 | package task 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/michaelmacinnis/adapted" 13 | "github.com/michaelmacinnis/oh/internal/common" 14 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 15 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 16 | "github.com/michaelmacinnis/oh/internal/common/interface/reference" 17 | "github.com/michaelmacinnis/oh/internal/common/interface/scope" 18 | "github.com/michaelmacinnis/oh/internal/common/struct/frame" 19 | "github.com/michaelmacinnis/oh/internal/common/type/list" 20 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 21 | "github.com/michaelmacinnis/oh/internal/common/type/str" 22 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 23 | ) 24 | 25 | const debug = false 26 | 27 | type monitor interface { 28 | Await(fn func(), t *T, ts ...*T) 29 | Execute(t *T, path string, argv []string, attr *os.ProcAttr) error 30 | Spawn(p, c *T, fn func()) 31 | Stopped(t *T) 32 | } 33 | 34 | // T (task) encapsulates a thread of execution. 35 | type T struct { 36 | job monitor 37 | *registers 38 | *state 39 | } 40 | 41 | // New creates a new task. 42 | func New(m monitor, c cell.I, f *frame.T) *T { 43 | t := &T{ 44 | registers: ®isters{ 45 | code: c, 46 | dump: pair.Null, 47 | frame: f, 48 | stack: done, 49 | }, 50 | job: m, 51 | state: fresh(), 52 | } 53 | 54 | return t 55 | } 56 | 57 | // CellValue resolves the name k and returns its value as a cell.I. 58 | func (t *T) CellValue(k string) cell.I { 59 | v := t.value(nil, k) 60 | if v == nil { 61 | return nil 62 | } 63 | 64 | return v 65 | } 66 | 67 | // Chdir changes the working directory by modifying PWD and OLDPWD in the current scope. 68 | func (t *T) Chdir(s string) cell.I { 69 | rv := sym.True 70 | 71 | _, r := t.frame.Resolve("PWD") 72 | oldwd := r.Get() 73 | 74 | wd := common.String(oldwd) 75 | 76 | if !filepath.IsAbs(s) { 77 | s = filepath.Join(wd, s) 78 | } 79 | 80 | err := os.Chdir(s) 81 | if err != nil { 82 | rv = pair.Null 83 | } else { 84 | t.frame.Scope().Export("PWD", sym.New(s)) 85 | t.frame.Scope().Export("OLDPWD", oldwd) 86 | } 87 | 88 | return rv 89 | } 90 | 91 | // Closure does as the name implies and creates a closure. 92 | func (t *T) Closure() *Closure { 93 | slabel := pair.Car(t.code) 94 | t.code = pair.Cdr(t.code) 95 | 96 | plabels := slabel 97 | if sym.Is(slabel) { 98 | plabels = pair.Car(t.code) 99 | t.code = pair.Cdr(t.code) 100 | } else { 101 | slabel = pair.Null 102 | } 103 | 104 | // TODO: Check plabels is a list of symbols. Last element can be a list. 105 | 106 | first := pair.Car(t.code) 107 | 108 | elabel := pair.Null 109 | if !pair.Is(first) { 110 | elabel = first 111 | t.code = pair.Cdr(t.code) 112 | } 113 | 114 | return &Closure{ 115 | Body: t.code, 116 | Labels: Labels{ 117 | Env: elabel, 118 | Params: plabels, 119 | Self: slabel, 120 | }, 121 | Op: Action(apply), 122 | Scope: t.frame.Scope(), 123 | } 124 | } 125 | 126 | // Environ returns key value pairs for stringable values in the form provided by os.Environ. 127 | func (t *T) Environ() []string { 128 | exported := map[string]string{} 129 | 130 | for f := t.registers.frame; f != nil; f = f.Previous() { 131 | for s := f.Scope(); s != nil; s = s.Enclosing() { 132 | for k, v := range s.Public().Exported() { 133 | if _, ok := exported[k]; !ok { 134 | exported[k] = v 135 | } 136 | } 137 | } 138 | } 139 | 140 | environ := make([]string, 0, len(exported)) 141 | for k, v := range exported { 142 | environ = append(environ, k+"="+v) 143 | } 144 | 145 | return environ 146 | } 147 | 148 | // Interrupt stops the task. 149 | func (t *T) Interrupt() { 150 | t.state.Stop(func() { 151 | t.stack = done 152 | }) 153 | } 154 | 155 | // Return is a helper used by actions to return a value. 156 | func (t *T) Return(c cell.I) Op { 157 | t.ReplaceResult(c) 158 | 159 | return t.PreviousOp() 160 | } 161 | 162 | // Run steps through a tasks operations until they are exhausted. 163 | func (t *T) Run() { 164 | t.Started() 165 | defer t.job.Stopped(t) 166 | 167 | s := t.Op() 168 | for t.state.Runnable() && s != nil { 169 | s = t.Step(s) 170 | } 171 | } 172 | 173 | // Step performs a single action and determines the next action. 174 | func (t *T) Step(s Op) (op Op) { 175 | defer func() { 176 | r := recover() 177 | if r == nil { 178 | return 179 | } 180 | 181 | errmsg := fmt.Sprintf("%v", r) 182 | t.code = list.New(sym.New("throw"), str.New(errmsg)) 183 | 184 | op = t.PushOp(Action(EvalCommand)) 185 | }() 186 | 187 | if debug { 188 | print("Stack: ") 189 | 190 | for p := t.stack; p != nil && p.op != nil; p = p.stack { 191 | print(opString(p.op)) 192 | print(" ") 193 | } 194 | 195 | println("") 196 | print("Dump: ") 197 | 198 | for p := t.dump; p != pair.Null; p = pair.Cdr(p) { 199 | c := pair.Car(p) 200 | if c == nil { 201 | print(" ") 202 | } else { 203 | print(pair.Car(p).Name()) 204 | print(" ") 205 | } 206 | } 207 | 208 | println("") 209 | print("Code: ") 210 | 211 | println(literal.String(t.code)) 212 | 213 | println("") 214 | } 215 | 216 | op = s.Perform(t) 217 | 218 | return op 219 | } 220 | 221 | // Stop stops a task but allows for it to be restarted. 222 | func (t *T) Stop() { 223 | t.state.Stop(nil) 224 | } 225 | 226 | func (t *T) expand(args cell.I) cell.I { 227 | l := pair.Null 228 | 229 | for ; args != pair.Null; args = pair.Cdr(args) { 230 | c := pair.Car(args) 231 | 232 | if !sym.Is(c) { 233 | l = list.Append(l, c) 234 | 235 | continue 236 | } 237 | 238 | s := common.String(c) 239 | 240 | path := t.tildeExpand(s) 241 | if !strings.ContainsAny(path, "*?[") { 242 | l = list.Append(l, sym.New(path)) 243 | 244 | continue 245 | } 246 | 247 | pwd := "" 248 | if !filepath.IsAbs(path) { 249 | pwd = t.stringValue("PWD") 250 | // path = filepath.Join(pwd, path) 251 | path = pwd + string(os.PathSeparator) + path 252 | pwd = filepath.Clean(pwd) 253 | } 254 | 255 | m, err := adapted.Glob(path) 256 | if err != nil || len(m) == 0 { 257 | panic("no matches found: " + s) 258 | } 259 | 260 | for _, v := range m { 261 | if pwd != "" { 262 | rel, err := filepath.Rel(pwd, v) 263 | if err == nil { 264 | v = rel 265 | } 266 | } 267 | 268 | l = list.Append(l, str.New(v)) 269 | } 270 | } 271 | 272 | return l 273 | } 274 | 275 | func (t *T) resolve(s scope.I, k string) cell.I { 276 | v := t.value(s, k) 277 | if v == nil { 278 | panic("'" + k + "' not defined") 279 | } 280 | 281 | return v 282 | } 283 | 284 | func (t *T) stringValue(k string) string { 285 | v := t.value(nil, k) 286 | if v == nil { 287 | return "" 288 | } 289 | 290 | return common.String(v) 291 | } 292 | 293 | func (t *T) tildeExpand(s string) string { 294 | if !strings.HasPrefix(s, "~") { 295 | return s 296 | } 297 | 298 | return filepath.Join(t.stringValue("HOME"), s[1:]) 299 | } 300 | 301 | func (t *T) value(s scope.I, k string) cell.I { 302 | var r reference.I 303 | 304 | if s != nil && s.Expose() != t.frame.Scope() { 305 | r = s.Lookup(k) 306 | } else { 307 | s, r = t.frame.Resolve(k) 308 | } 309 | 310 | if r == nil { 311 | return nil 312 | } 313 | 314 | v := r.Get() 315 | if c, ok := v.(command); ok { 316 | v = bind(c, s) 317 | } 318 | 319 | return v 320 | } 321 | -------------------------------------------------------------------------------- /internal/reader/lexer/lexer_internal_test.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/michaelmacinnis/oh/internal/common/struct/loc" 7 | "github.com/michaelmacinnis/oh/internal/common/struct/token" 8 | ) 9 | 10 | func TestBackground(t *testing.T) { 11 | h := setup(t, "Background") 12 | 13 | h.scan("1 &\n", 14 | h.symbol("1"), 15 | h.literal(" "), 16 | h.literal("&"), 17 | h.literal("\n"), 18 | nil, 19 | ) 20 | } 21 | 22 | func TestDollarDollar(t *testing.T) { 23 | h := setup(t, "DollarDollar") 24 | 25 | h.scan("$$\n", 26 | h.literal("$"), 27 | h.symbol("$"), 28 | h.literal("\n"), 29 | nil, 30 | ) 31 | } 32 | 33 | func TestImplicitConcatenation(t *testing.T) { 34 | h := setup(t, "ImplicitConcatenation") 35 | 36 | h.scan("1'foo'\"bar\"2\n", 37 | h.symbol("1"), 38 | h.other(token.SingleQuoted, "'foo'"), 39 | h.other(token.DoubleQuoted, "\"bar\""), 40 | h.symbol("2"), 41 | h.literal("\n"), 42 | nil, 43 | ) 44 | 45 | h.scan("stdout=stderr\n", 46 | h.symbol("stdout"), 47 | h.symbol("="), 48 | h.symbol("stderr"), 49 | h.literal("\n"), 50 | nil, 51 | ) 52 | } 53 | 54 | func TestImplicitContinuation(t *testing.T) { 55 | h := setup(t, "ImplicitContinuation") 56 | 57 | for _, op := range []string{ 58 | "&&", "|", "|&", "||", 59 | } { 60 | v := "1 " + op + "\n 2\n" 61 | h.scan(v, 62 | h.symbol("1"), 63 | h.literal(" "), 64 | h.literal(op), 65 | h.escapedNewline(), 66 | h.silentSpace(1), 67 | h.symbol("2"), 68 | h.literal("\n"), 69 | nil, 70 | ) 71 | } 72 | } 73 | 74 | func TestInfixOperators(t *testing.T) { 75 | h := setup(t, "InfixOperators") 76 | 77 | for _, op := range []string{ 78 | "<", ">", ">&", ">&|", ">>", 79 | ">>&", ">|", "|<", "|>", 80 | } { 81 | v := "1 " + op + " 2\n" 82 | h.scan(v, 83 | h.symbol("1"), 84 | h.literal(" "), 85 | h.literal(op), 86 | h.silentSpace(1), 87 | h.symbol("2"), 88 | h.literal("\n"), 89 | nil, 90 | ) 91 | } 92 | 93 | for _, op := range []string{ 94 | "<", ">", ">&", ">&|", ">>", 95 | ">>&", ">|", "|<", "|>", 96 | } { 97 | // The newline before '2' will cause 98 | // the parser to throw an error, but 99 | // we want to test that the lexer 100 | // doesn't treat these operators as 101 | // implicit line continuations. 102 | v := "1 " + op + "\n2\n" 103 | h.scan(v, 104 | h.symbol("1"), 105 | h.literal(" "), 106 | h.literal(op), 107 | h.literal("\n"), 108 | h.symbol("2"), 109 | h.literal("\n"), 110 | nil, 111 | ) 112 | } 113 | } 114 | 115 | func TestMeta(t *testing.T) { 116 | h := setup(t, "Meta") 117 | 118 | h.scan("(|number 10|)\n", 119 | h.literal("(|"), 120 | h.symbol("number"), 121 | h.literal(" "), 122 | h.symbol("10"), 123 | h.literal("|)"), 124 | h.literal("\n"), 125 | nil, 126 | ) 127 | } 128 | 129 | func TestTrailingDollar(t *testing.T) { 130 | h := setup(t, "TrailingDollar") 131 | 132 | h.scan("1$ 2\n", 133 | h.symbol("1"), 134 | h.symbol("$"), 135 | h.literal(" "), 136 | h.symbol("2"), 137 | h.literal("\n"), 138 | nil, 139 | ) 140 | } 141 | 142 | type harness struct { 143 | index int 144 | lexer *T 145 | source loc.T 146 | t *testing.T 147 | } 148 | 149 | //nolint:gochecknoglobals 150 | var skip = token.New(token.Error, "", &loc.T{ 151 | Char: 0, 152 | Line: 0, 153 | Name: "", 154 | }) 155 | 156 | func setup(t *testing.T, label string) *harness { 157 | return &harness{ 158 | index: 1, 159 | lexer: New(label), 160 | source: loc.T{ 161 | Char: 1, 162 | Line: 1, 163 | Name: label, 164 | }, 165 | t: t, 166 | } 167 | } 168 | 169 | func (h *harness) expect(tokens ...*token.T) { 170 | for _, e := range tokens { 171 | if e == skip { 172 | continue 173 | } 174 | 175 | a := h.lexer.Token() 176 | 177 | switch { 178 | case a == e: 179 | continue 180 | case a != nil && e != nil && a.String() == e.String(): 181 | continue 182 | case a == nil: 183 | h.t.Fatalf("Expected %v but there are no tokens", e) 184 | case e == nil: 185 | h.t.Fatalf("Expected no tokens; got %v", a) 186 | case *a != *e: 187 | h.t.Fatalf("Expected %v; got %v", e, a) 188 | } 189 | } 190 | } 191 | 192 | func (h *harness) literal(s string) *token.T { 193 | h.source.Char = h.index 194 | h.index += len(s) 195 | 196 | id, found := map[string]token.Class{ 197 | " ": token.Space, 198 | "&": token.Background, 199 | "&&": token.Andf, 200 | "(|": token.MetaOpen, 201 | "<": token.Redirect, 202 | ">": token.Redirect, 203 | ">&": token.Redirect, 204 | ">&|": token.Redirect, 205 | ">>": token.Redirect, 206 | ">>&": token.Redirect, 207 | ">|": token.Redirect, 208 | "|": token.Pipe, 209 | "|&": token.Pipe, 210 | "|)": token.MetaClose, 211 | "|<": token.Substitute, 212 | "|>": token.Substitute, 213 | "||": token.Orf, 214 | }[s] 215 | if !found { 216 | id = token.Class(s[0]) 217 | } 218 | 219 | if op := operator(s); op != "" { 220 | s = op 221 | } 222 | 223 | if s == "\n" { 224 | h.index = 1 225 | } 226 | 227 | location := h.source 228 | t := token.New(id, s, &location) 229 | 230 | if s == "\n" { 231 | h.source.Line++ 232 | } 233 | 234 | return t 235 | } 236 | 237 | func (h *harness) escapedNewline() *token.T { 238 | h.index = 1 239 | h.source.Line++ 240 | 241 | return skip 242 | } 243 | 244 | func (h *harness) silentSpace(n int) *token.T { 245 | h.index += n 246 | 247 | return skip 248 | } 249 | 250 | func (h *harness) other(id token.Class, s string) *token.T { 251 | h.source.Char = h.index 252 | h.index += len(s) 253 | 254 | location := h.source 255 | 256 | return token.New(id, s, &location) 257 | } 258 | 259 | func (h *harness) scan(s string, tokens ...*token.T) { 260 | h.lexer.Scan(s) 261 | h.expect(tokens...) 262 | } 263 | 264 | func (h *harness) symbol(s string) *token.T { 265 | h.source.Char = h.index 266 | h.index += len(s) 267 | 268 | location := h.source 269 | 270 | return token.New(token.Symbol, s, &location) 271 | } 272 | -------------------------------------------------------------------------------- /internal/reader/parser/parser.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | // Package parser provides a recursive descent parser for the oh language. 4 | package parser 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/michaelmacinnis/adapted" 12 | "github.com/michaelmacinnis/oh/internal/common" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 14 | "github.com/michaelmacinnis/oh/internal/common/struct/token" 15 | "github.com/michaelmacinnis/oh/internal/common/type/list" 16 | "github.com/michaelmacinnis/oh/internal/common/type/num" 17 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 18 | "github.com/michaelmacinnis/oh/internal/common/type/status" 19 | "github.com/michaelmacinnis/oh/internal/common/type/str" 20 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 21 | ) 22 | 23 | // T holds the state of the parser. 24 | type T struct { 25 | ahead int // Lookahead count. 26 | emit func(cell.I) // Function to call to emit a parsed command. 27 | item func() *token.T // Function to call to get another token. 28 | token *token.T // Token lookahead. 29 | 30 | // Completion state. 31 | current cell.I // The command being parsed, so far. 32 | } 33 | 34 | // New creates a new parser. 35 | // It connects a producer of tokens with a consumer of cells. 36 | func New(emit func(cell.I), item func() *token.T) *T { 37 | return &T{emit: emit, item: item, current: pair.Null} 38 | } 39 | 40 | // Copy copies the current parser but replaces its emit and item functions. 41 | func (p *T) Copy(emit func(cell.I), item func() *token.T) *T { 42 | c := *p 43 | 44 | c.emit = emit 45 | c.item = item 46 | 47 | return &c 48 | } 49 | 50 | // Current returns the command currently being parsed. 51 | func (p *T) Current() cell.I { 52 | return p.current 53 | } 54 | 55 | // Parse consumes tokens and emits cells until there are no more tokens. 56 | func (p *T) Parse() (err error) { 57 | defer func() { 58 | r := recover() 59 | if r == nil { 60 | return 61 | } 62 | 63 | switch r := r.(type) { 64 | case error: 65 | err = r 66 | case string: 67 | err = common.Error(r) 68 | default: 69 | err = fmt.Errorf("unexpected error: %v", r) //nolint:goerr113 70 | } 71 | }() 72 | 73 | for t := p.peek(); t != nil; t = p.peek() { 74 | if t.Is('\n') { 75 | p.consume() 76 | 77 | continue 78 | } 79 | 80 | p.emit(p.possibleBackground()) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (p *T) consume() *token.T { 87 | if p.ahead == 0 { 88 | panic("nothing to consume.") 89 | } 90 | 91 | t := p.token 92 | 93 | p.ahead = 0 94 | p.token = nil 95 | 96 | return t 97 | } 98 | 99 | func (p *T) check(c cell.I) cell.I { 100 | if c == nil { 101 | t := p.peek() 102 | 103 | loc := t.Source() 104 | l := loc.Name 105 | x := strconv.Itoa(loc.Char) 106 | y := strconv.Itoa(loc.Line) 107 | 108 | panic(l + ":" + y + ":" + x + ": unexpected '" + t.Source().Text + "'") 109 | } 110 | 111 | return c 112 | } 113 | 114 | func (p *T) expect(cs ...token.Class) { 115 | if p.peek().Is(cs...) { 116 | p.consume() 117 | 118 | return 119 | } 120 | 121 | // Make a nice error message. 122 | n := len(cs) 123 | e := make([]string, n) 124 | 125 | for i, c := range cs[:n-1] { 126 | e[i] = c.String() 127 | } 128 | 129 | l := cs[n-1].String() 130 | if n > 2 { //nolint:gomnd 131 | l = ", or " + l 132 | } else if n > 1 { 133 | l = " or " + l 134 | } 135 | 136 | l = strings.Join(e, ", ") + l 137 | s := p.peek().Value() 138 | 139 | panic("expected " + l + ` got "` + s + `"`) 140 | } 141 | 142 | func (p *T) peek() *token.T { 143 | if p.ahead > 0 { 144 | return p.token 145 | } 146 | 147 | t := p.item() 148 | 149 | p.token = t 150 | p.ahead = 1 151 | 152 | return t 153 | } 154 | 155 | // T state functions. 156 | 157 | // ::= '&'? 158 | func (p *T) possibleBackground() cell.I { 159 | c := p.command() 160 | 161 | t := p.peek() 162 | if t.Is(token.Background) { 163 | p.consume() 164 | 165 | c = list.New(sym.Token(t), c) 166 | } 167 | 168 | return c 169 | } 170 | 171 | // ::= (Orf )* . 172 | func (p *T) command() cell.I { 173 | c := p.possibleAndf() 174 | 175 | t := p.peek() 176 | if t.Is(token.Orf) { 177 | c = list.New(sym.Token(t), c) 178 | 179 | for p.peek().Is(token.Orf) { 180 | p.consume() 181 | c = list.Append(c, p.possibleAndf()) 182 | } 183 | } 184 | 185 | return c 186 | } 187 | 188 | // ::= (Andf )* . 189 | func (p *T) possibleAndf() cell.I { 190 | c := p.possiblePipeline() 191 | 192 | t := p.peek() 193 | if t.Is(token.Andf) { 194 | c = list.New(sym.Token(t), c) 195 | 196 | for p.peek().Is(token.Andf) { 197 | p.consume() 198 | c = list.Append(c, p.possiblePipeline()) 199 | } 200 | } 201 | 202 | return c 203 | } 204 | 205 | // ::= (Pipe )? 206 | func (p *T) possiblePipeline() cell.I { 207 | c := p.possibleSequence() 208 | 209 | if p.peek().Is(token.Pipe) { 210 | s := sym.Token(p.consume()) 211 | 212 | c = pair.Cons(p.possiblePipeline(), c) 213 | c = pair.Cons(s, c) 214 | } 215 | 216 | return c 217 | } 218 | 219 | // ::= (';' )* . 220 | func (p *T) possibleSequence() cell.I { 221 | c := p.possibleRedirection() 222 | 223 | if p.peek().Is(';') { 224 | c = list.New(sym.New("block"), c) 225 | 226 | for p.peek().Is(';') { 227 | p.consume() 228 | 229 | c = list.Append(c, p.possibleRedirection()) 230 | } 231 | } 232 | 233 | return c 234 | } 235 | 236 | // ::= (Redirect )* . 237 | func (p *T) possibleRedirection() cell.I { 238 | c := p.possibleSubstitution() 239 | 240 | for p.peek().Is(token.Redirect) { 241 | s := sym.Token(p.consume()) 242 | c = list.New(s, p.check(p.implicitJoin(p.element())), c) 243 | 244 | for p.peek().Is(token.Space) { 245 | p.consume() 246 | } 247 | } 248 | 249 | return c 250 | } 251 | 252 | // ::= (Substitute ')' ?)* . 253 | func (p *T) possibleSubstitution() cell.I { 254 | c := p.statement() 255 | if c == nil { 256 | return c 257 | } 258 | 259 | if p.peek().Is(token.Substitute) { 260 | c = pair.Cons(sym.New("process-substitution"), c) 261 | 262 | for p.peek().Is(token.Substitute) { 263 | s := sym.Token(p.consume()) 264 | l := pair.Cons(s, p.element()) 265 | c = list.Append(c, l) 266 | 267 | if !p.peek().Is(token.Substitute) { 268 | s := p.statement() 269 | if s != nil { 270 | c = list.Join(c, s) 271 | } 272 | } 273 | } 274 | } 275 | 276 | return c 277 | } 278 | 279 | func (p *T) braces() (c cell.I) { 280 | if p.peek().Is('{') { 281 | p.consume() 282 | 283 | n := p.peek() 284 | 285 | switch { 286 | case n.Is('\n'): 287 | p.consume() 288 | 289 | c = p.subStatement() 290 | case n.Is('{'): 291 | c = p.braces() 292 | p.expect('}') 293 | default: 294 | c = p.implicitJoin(p.element()) 295 | c = pair.Cons(c, pair.Null) 296 | 297 | p.expect('}') 298 | } 299 | } 300 | 301 | return 302 | } 303 | 304 | func (p *T) assignments() (c, l cell.I) { 305 | l = pair.Null 306 | 307 | for { 308 | for p.peek().Is(token.Space) { 309 | p.consume() 310 | } 311 | 312 | c = p.braces() 313 | if c != nil { 314 | break 315 | } 316 | 317 | c = p.element() 318 | if c == nil { 319 | break 320 | } 321 | 322 | e := p.peek() 323 | if sym.Is(c) && e.Is(token.Symbol) && e.Value() == "=" { 324 | p.consume() 325 | 326 | v := p.check(p.implicitJoin(p.element())) 327 | 328 | l = list.Append(l, list.New(sym.New("export"), c, v)) 329 | 330 | continue 331 | } else { 332 | c = p.implicitJoin(c) 333 | 334 | break 335 | } 336 | } 337 | 338 | return c, l 339 | } 340 | 341 | func (p *T) statement() (c cell.I) { 342 | // Reset current command. 343 | p.current = pair.Null 344 | 345 | c, l := p.assignments() 346 | if l != pair.Null { 347 | defer func() { 348 | if c != nil { 349 | c = list.Join(l, pair.Cons(c, pair.Null)) 350 | } else { 351 | c = l 352 | } 353 | 354 | c = pair.Cons(sym.New("block"), c) 355 | }() 356 | } 357 | 358 | if c == nil { 359 | return 360 | } 361 | 362 | c = pair.Cons(c, pair.Null) 363 | 364 | for { 365 | p.current = c 366 | 367 | if p.peek().Is(token.Space) { 368 | p.consume() 369 | 370 | continue 371 | } 372 | 373 | t := p.braces() 374 | if t == nil { 375 | t = p.implicitJoin(p.element()) 376 | if t == nil { 377 | break 378 | } 379 | 380 | t = pair.Cons(t, pair.Null) 381 | } 382 | 383 | c = list.Join(c, t) 384 | } 385 | 386 | return c 387 | } 388 | 389 | func (p *T) subStatement() cell.I { 390 | c := p.block() 391 | 392 | p.expect('}') 393 | 394 | for p.peek().Is(token.Space) { 395 | p.consume() 396 | } 397 | 398 | s := p.statement() 399 | if s != nil { 400 | c = list.Join(c, s) 401 | } 402 | 403 | return c 404 | } 405 | 406 | func (p *T) block() cell.I { 407 | c := pair.Null 408 | 409 | for !p.peek().Is('}') { 410 | if p.peek().Is('\n') { 411 | p.consume() 412 | 413 | continue 414 | } 415 | 416 | c = list.Append(c, p.check(p.possibleBackground())) 417 | } 418 | 419 | return c 420 | } 421 | 422 | func (p *T) implicitJoin(c cell.I) cell.I { 423 | if c == nil { 424 | return nil 425 | } 426 | 427 | l := list.New(c) 428 | 429 | for t := p.element(); t != nil; t = p.element() { 430 | l = list.Append(l, t) 431 | } 432 | 433 | if list.Length(l) == 1 { 434 | return c 435 | } 436 | 437 | l = pair.Cons(sym.New(""), l) 438 | 439 | return pair.Cons(sym.New("mend"), l) 440 | } 441 | 442 | func (p *T) element() cell.I { 443 | if p.peek().Is('`') { 444 | p.consume() 445 | 446 | c := p.check(p.value()) 447 | 448 | c = pair.Cons(sym.New("capture"), list.New(c)) 449 | c = list.New(sym.New("splice"), c) 450 | 451 | return c 452 | } 453 | 454 | return p.expression() 455 | } 456 | 457 | func (p *T) expression() cell.I { 458 | if p.peek().Is('$') { 459 | p.consume() 460 | 461 | c := p.braces() 462 | if c == nil { 463 | c = p.check(p.expression()) 464 | } else { 465 | c = pair.Car(c) 466 | } 467 | 468 | return list.New(sym.New("resolve"), p.check(c)) 469 | } 470 | 471 | return p.value() 472 | } 473 | 474 | func (p *T) meta(c cell.I) cell.I { 475 | t := pair.Car(c) 476 | 477 | if !sym.Is(t) { 478 | panic("meta command must start with a symbol not " + t.Name()) 479 | } 480 | 481 | var create func(string) cell.I = nil 482 | 483 | switch sym.To(t).String() { 484 | case "cons": 485 | return pair.Cons(pair.Cadr(c), pair.Caddr(c)) 486 | 487 | case "number": 488 | create = num.New 489 | 490 | case "status": 491 | create = status.New 492 | 493 | case "symbol": 494 | create = sym.New 495 | } 496 | 497 | if create == nil { 498 | panic("invalid meta command") 499 | } 500 | 501 | t = pair.Cadr(c) 502 | 503 | arg, ok := t.(fmt.Stringer) 504 | if ok { 505 | return create(arg.String()) 506 | } 507 | 508 | // TODO: What case are we handling here? 509 | return num.New(arg.String()) 510 | } 511 | 512 | func (p *T) value() cell.I { 513 | t := p.peek() 514 | 515 | meta := false 516 | if t.Is(token.MetaOpen) { 517 | meta = true 518 | } else if !t.Is('(') { 519 | return p.word() 520 | } 521 | 522 | p.consume() 523 | 524 | c := p.command() 525 | if c == nil { 526 | t := p.peek() 527 | if t.Is(')') { 528 | p.consume() 529 | 530 | return pair.Null 531 | } 532 | 533 | panic("unexpected '" + t.Source().Text + "'") 534 | } 535 | 536 | if meta { 537 | p.expect(token.MetaClose) 538 | 539 | return p.meta(c) 540 | } 541 | 542 | p.expect(')') 543 | 544 | return c 545 | } 546 | 547 | func (p *T) symbol(t *token.T) cell.I { 548 | if t.Is(token.Symbol) { 549 | p.consume() 550 | 551 | return sym.Token(t) 552 | } 553 | 554 | return nil 555 | } 556 | 557 | func (p *T) word() cell.I { 558 | t := p.peek() 559 | if t.Is(token.DollarSingleQuoted) { 560 | p.consume() 561 | 562 | text := t.Value() 563 | 564 | s, err := adapted.ActualBytes(text[2 : len(text)-1]) 565 | if err != nil { 566 | panic(err.Error()) 567 | } 568 | 569 | return str.New(s) 570 | } 571 | 572 | if t.Is(token.DoubleQuoted) { 573 | p.consume() 574 | 575 | text := t.Value() 576 | 577 | s, err := adapted.ActualBytes(text[1 : len(text)-1]) 578 | if err != nil { 579 | panic(err.Error()) 580 | } 581 | 582 | return list.New(sym.New("interpolate"), str.New(s)) 583 | } 584 | 585 | if t.Is(token.SingleQuoted) { 586 | p.consume() 587 | 588 | s := t.Value() 589 | 590 | return str.New(s[1 : len(s)-1]) 591 | } 592 | 593 | return p.symbol(t) 594 | } 595 | -------------------------------------------------------------------------------- /internal/reader/parser/parser_internal_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 7 | "github.com/michaelmacinnis/oh/internal/common/interface/literal" 8 | "github.com/michaelmacinnis/oh/internal/engine/boot" 9 | "github.com/michaelmacinnis/oh/internal/reader/lexer" 10 | ) 11 | 12 | func TestBananaClipCons(t *testing.T) { 13 | check(t, "(|cons 1 2|)\n") 14 | } 15 | 16 | func TestBananaClipConsNils(t *testing.T) { 17 | check(t, "(|cons () ()|)\n") 18 | } 19 | 20 | func TestBananaClipNil(t *testing.T) { 21 | check(t, "()\n") 22 | } 23 | 24 | func TestBananaClipNumber(t *testing.T) { 25 | check(t, "(|number 42|)\n") 26 | } 27 | 28 | func check(t *testing.T, s string) { 29 | l := lexer.New("test") 30 | 31 | l.Scan(s) 32 | 33 | p := "" 34 | 35 | New(func(c cell.I) { 36 | s := literal.String(c) + "\n" 37 | p += s 38 | }, l.Token).Parse() 39 | 40 | m := lexer.New("test") 41 | 42 | m.Scan(p) 43 | 44 | r := "" 45 | 46 | New(func(c cell.I) { 47 | s := literal.String(c) + "\n" 48 | r += s 49 | }, m.Token).Parse() 50 | 51 | if p != r { 52 | t.Fatalf("Parsed (%s) and reparsed (%s) do not match", p, r) 53 | } 54 | } 55 | 56 | // TODO: Convert these into table-driven tests. 57 | // TODO: Write tests that don't involve reparsing. 58 | 59 | func TestBackground(t *testing.T) { 60 | check(t, "sleep 5; echo tea is ready!&\n") 61 | } 62 | 63 | func TestBoot(t *testing.T) { 64 | check(t, boot.Script()) 65 | } 66 | 67 | func TestMultipleRedirections(t *testing.T) { 68 | check(t, "tr ' ' '\\n' < foo > bar\n") 69 | } 70 | 71 | func TestPipe(t *testing.T) { 72 | check(t, "ls | grep .go\n") 73 | } 74 | 75 | func TestPreStatementAssignment(t *testing.T) { 76 | check(t, "GOOS=linux GOARCH=ppc64 ./bootstrap.bash\n") 77 | } 78 | 79 | func TestSimpleCommand(t *testing.T) { 80 | check(t, "ls -la\n") 81 | } 82 | 83 | func TestTrailingDollar(t *testing.T) { 84 | check(t, "go test -run=^$\n") 85 | } 86 | -------------------------------------------------------------------------------- /internal/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 5 | "github.com/michaelmacinnis/oh/internal/common/struct/token" 6 | "github.com/michaelmacinnis/oh/internal/reader/lexer" 7 | "github.com/michaelmacinnis/oh/internal/reader/parser" 8 | ) 9 | 10 | // T (reader) encapsulates the oh lexer and parser. 11 | type T struct { 12 | e chan error 13 | i chan string 14 | o chan cell.I 15 | p *parser.T 16 | s *lexer.T 17 | } 18 | 19 | type reader = T 20 | 21 | // New creates a new reader for name. 22 | func New(name string) *T { 23 | r := &T{ 24 | e: make(chan error), 25 | i: make(chan string), 26 | o: make(chan cell.I), 27 | s: lexer.New(name), 28 | } 29 | 30 | var v cell.I 31 | 32 | r.p = parser.New(func(c cell.I) { 33 | v = c 34 | }, func() *token.T { 35 | t := r.s.Token() 36 | 37 | for t == nil { 38 | r.o <- v 39 | 40 | v = nil 41 | 42 | if !r.next() { 43 | close(r.o) 44 | } 45 | 46 | t = r.s.Token() 47 | } 48 | 49 | return t 50 | }) 51 | 52 | go r.start() 53 | 54 | return r 55 | } 56 | 57 | // Close terminates the reader. 58 | func (r *reader) Close() { 59 | close(r.i) 60 | } 61 | 62 | // Lexer returns the reader's internal lexer.T. 63 | func (r *reader) Lexer() *lexer.T { 64 | return r.s 65 | } 66 | 67 | // Parser returns the readers's internal parser.T. 68 | func (r *reader) Parser() *parser.T { 69 | return r.p 70 | } 71 | 72 | // Scan reads the line and returns a cell.I on a complete parse or nil otherwise. 73 | // If scan encounters any error it returns the error. 74 | func (r *reader) Scan(line string) (c cell.I, err error) { 75 | r.i <- line 76 | 77 | select { 78 | case c = <-r.o: 79 | case err = <-r.e: 80 | } 81 | 82 | return c, err 83 | } 84 | 85 | func (r *reader) next() bool { 86 | line, ok := <-r.i 87 | if ok { 88 | r.s.Scan(line) 89 | } 90 | 91 | return ok 92 | } 93 | 94 | func (r *reader) start() { 95 | r.next() 96 | 97 | r.e <- r.p.Parse() 98 | close(r.e) 99 | } 100 | -------------------------------------------------------------------------------- /internal/system/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // Check ensures that all executables in path's directory are cached. 10 | func Check(path string) { 11 | resultq := make(chan bool) 12 | 13 | dirname, basename := filepath.Split(path) 14 | requestq <- func() { 15 | for _, p := range executables[dirname] { 16 | if p == basename { 17 | resultq <- true 18 | 19 | break 20 | } 21 | } 22 | close(resultq) 23 | } 24 | 25 | if <-resultq { 26 | Files(dirname) 27 | } 28 | } 29 | 30 | // Executables returns executables (and directories) in dirname and schedules a rescan or dirname. 31 | func Executables(dirname string) []string { 32 | resultq := make(chan []string) 33 | 34 | dirname = filepath.Clean(dirname) 35 | requestq <- func() { 36 | resultq <- executables[dirname] 37 | close(resultq) 38 | } 39 | 40 | res := <-resultq 41 | if res == nil { 42 | go Files(dirname) 43 | } 44 | 45 | return res 46 | } 47 | 48 | // Files caches executables (and directories) and returns files (and directories) found in dirname. 49 | func Files(dirname string) []string { 50 | dirname = filepath.Clean(dirname) 51 | 52 | max := strings.Count(dirname, pathSeparator) + 1 53 | 54 | e := []string{} 55 | f := []string{} 56 | 57 | done := make(chan struct{}) 58 | 59 | requestq <- func() { 60 | if _, ok := executables[dirname]; !ok { 61 | delete(executables, dirname) 62 | } 63 | close(done) 64 | } 65 | 66 | <-done 67 | 68 | original := dirname 69 | 70 | dirname, err := filepath.EvalSymlinks(dirname) 71 | if err != nil { 72 | dirname = original 73 | } 74 | 75 | _ = filepath.Walk(dirname, func(p string, i os.FileInfo, err error) error { 76 | if p == dirname { 77 | return nil 78 | } 79 | 80 | depth := strings.Count(p, pathSeparator) 81 | if depth > max { 82 | if i.IsDir() { 83 | return filepath.SkipDir 84 | } 85 | 86 | return nil 87 | } else if depth < max { 88 | return nil 89 | } 90 | 91 | if original != dirname { 92 | p = strings.Replace(p, dirname, original, 1) 93 | } 94 | 95 | switch { 96 | case p != pathSeparator && i.IsDir(): 97 | p += pathSeparator 98 | e = append(e, p) 99 | 100 | case i.Mode()&0o111 != 0: 101 | e = append(e, p) 102 | } 103 | 104 | f = append(f, p) 105 | 106 | return nil 107 | }) 108 | 109 | requestq <- func() { 110 | if len(e) > 0 { 111 | executables[dirname] = e 112 | } 113 | } 114 | 115 | return f 116 | } 117 | 118 | // Populate scans each directory in the colon-separated list of dirnames. 119 | func Populate(dirnames string) { 120 | for _, dirname := range strings.Split(dirnames, pathListSeparator) { 121 | if dirname == "" { 122 | dirname = "." 123 | } else { 124 | dirname = filepath.Clean(dirname) 125 | } 126 | 127 | stat, err := os.Stat(dirname) 128 | if err != nil || !stat.IsDir() { 129 | continue 130 | } 131 | 132 | Files(dirname) 133 | } 134 | } 135 | 136 | //nolint:gochecknoglobals 137 | var ( 138 | executables = map[string][]string{} 139 | pathListSeparator = string(os.PathListSeparator) 140 | pathSeparator = string(os.PathSeparator) 141 | requestq chan func() 142 | ) 143 | 144 | func init() { //nolint:gochecknoinits 145 | requestq = make(chan func(), 1) 146 | 147 | go service() 148 | } 149 | 150 | func service() { 151 | for { 152 | (<-requestq)() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /internal/system/history/history.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // Load loads any saved command history. 9 | func Load(read func(r io.Reader) (int, error)) error { 10 | f, err := file(os.Open) 11 | if err != nil { 12 | // We may not find a history file. 13 | return nil 14 | } 15 | 16 | _, err = read(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return f.Close() 22 | } 23 | 24 | // Save saves the current command history. 25 | func Save(write func(w io.Writer) (int, error)) error { 26 | f, err := file(os.Create) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | _, err = write(f) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return f.Close() 37 | } 38 | -------------------------------------------------------------------------------- /internal/system/history/os_unix.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 4 | // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris 5 | 6 | package history 7 | 8 | import ( 9 | "os" 10 | "path" 11 | ) 12 | 13 | func file(op func(string) (*os.File, error)) (*os.File, error) { 14 | s, ok := os.LookupEnv("OH_HISTORY") 15 | if !ok { 16 | s = path.Join(os.Getenv("HOME"), ".oh-history") 17 | } 18 | 19 | return op(s) 20 | } 21 | -------------------------------------------------------------------------------- /internal/system/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/docopt/docopt-go" 7 | "github.com/mattn/go-isatty" 8 | ) 9 | 10 | //nolint:gochecknoglobals 11 | var ( 12 | args []string 13 | command string 14 | interactive bool 15 | monitor bool 16 | script string 17 | terminal int 18 | version bool 19 | 20 | usage = `oh 21 | 22 | Usage: 23 | oh [-m] SCRIPT [ARGUMENTS...] 24 | oh [-m] -c COMMAND [NAME [ARGUMENTS...]] 25 | oh [-im] [-s [ARGUMENTS...]] 26 | oh -h 27 | oh -v 28 | 29 | Arguments: 30 | ARGUMENTS Positional parameters. 31 | SCRIPT Path to oh script. Also used as the value for $0. 32 | NAME Override $0. Otherwise, $0 is set to name used to invoke oh. 33 | 34 | Options: 35 | -c, --command=COMMAND Run the specified command. 36 | -m, --monitor Invert job control mode. 37 | -i, --interactive Disable interactive mode. 38 | -s, --stdin Read commands from stdin. 39 | -h, --help Display this help. 40 | -v, --version Print oh version. 41 | 42 | If oh's stdin is a TTY, and oh was invoked with no non-option operands or 43 | oh was explicitly directed to evaluate commands from stdin, interactive and 44 | job control features are enabled. Otherwise, these features are disabled. 45 | ` 46 | ) 47 | 48 | // Args returns positional parameters (if any). 49 | func Args() []string { 50 | return args 51 | } 52 | 53 | // Command returns the command specified (if any). 54 | func Command() string { 55 | return command 56 | } 57 | 58 | // Interactive returns true if oh should run in interactive mode. 59 | func Interactive() bool { 60 | return interactive 61 | } 62 | 63 | // Parse parses the command line options for this invocation of oh. 64 | func Parse() { 65 | docopt.DefaultParser.OptionsFirst = true 66 | 67 | opts, err := docopt.ParseDoc(usage) 68 | if err != nil { 69 | // Error in the usage doc. This should never happen. 70 | panic(err.Error()) 71 | } 72 | 73 | script = "" 74 | 75 | command, _ = opts.String("--command") 76 | 77 | name, _ := opts.String("NAME") 78 | if name == "" { 79 | name = os.Args[0] 80 | } 81 | 82 | path, _ := opts.String("SCRIPT") 83 | if path != "" { 84 | command = "source " + path 85 | name = path 86 | script = path 87 | } else if command == "" && isatty.IsTerminal(os.Stdin.Fd()) { 88 | interactive = true 89 | monitor = true 90 | terminal = int(os.Stdin.Fd()) 91 | } 92 | 93 | args, _ = opts["ARGUMENTS"].([]string) 94 | args = append([]string{name}, args...) 95 | 96 | invertInteractive, _ := opts.Bool("--interactive") 97 | interactive = interactive != invertInteractive 98 | 99 | invertMonitor, _ := opts.Bool("--monitor") 100 | monitor = monitor != invertMonitor 101 | 102 | version, _ = opts.Bool("--version") 103 | } 104 | 105 | // Script returns the script name (if any). 106 | func Script() string { 107 | return script 108 | } 109 | 110 | // Monitor returns true if job control features should be enabled. 111 | func Monitor() bool { 112 | return monitor 113 | } 114 | 115 | // Terminal returns the terminal's integer file descriptor. 116 | func Terminal() int { 117 | return terminal 118 | } 119 | 120 | // Version returns true if oh's version was request. 121 | func Version() bool { 122 | return version 123 | } 124 | -------------------------------------------------------------------------------- /internal/system/process/process_unix.go: -------------------------------------------------------------------------------- 1 | // Released under an MIT license. See LICENSE. 2 | 3 | //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 4 | // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris 5 | 6 | package process 7 | 8 | import ( 9 | "os" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | //nolint:gochecknoglobals 15 | var ( 16 | Platform = "unix" 17 | 18 | // Umask sets and returns the current umask. 19 | Umask = unix.Umask 20 | 21 | id = unix.Getpid() 22 | group, _ = unix.Getpgid(id) 23 | terminal = int(os.Stdin.Fd()) 24 | ) 25 | 26 | // BecomeForegroundGroup performs the Unix incantations necessary to put the current process in the foreground. 27 | func BecomeForegroundGroup() (err error) { 28 | for group != ForegroundGroup() { 29 | err = unix.Kill(-group, unix.SIGTTIN) 30 | if err != nil { 31 | return 32 | } 33 | 34 | group, err = unix.Getpgid(id) 35 | if err != nil { 36 | return 37 | } 38 | } 39 | 40 | if id != group { 41 | err = unix.Setpgid(id, id) 42 | if err != nil { 43 | return 44 | } 45 | 46 | group = id 47 | } 48 | 49 | SetForegroundGroup(group) 50 | 51 | return 52 | } 53 | 54 | // Continue send a SIGCONT to the process ID pid. 55 | func Continue(pid int) { 56 | _ = unix.Kill(pid, unix.SIGCONT) 57 | } 58 | 59 | // ForegroundGroup returns the current foreground group ID. 60 | func ForegroundGroup() int { 61 | group, err := unix.IoctlGetInt(terminal, unix.TIOCGPGRP) 62 | if err != nil { 63 | return 0 64 | } 65 | 66 | return group 67 | } 68 | 69 | // Group returns the group ID for the current process. 70 | func Group() int { 71 | return group 72 | } 73 | 74 | // ID returns the process ID for the current process. 75 | func ID() int { 76 | return id 77 | } 78 | 79 | // Interrupt sends a SIGINT to the process ID pid. 80 | func Interrupt(pid int) { 81 | _ = unix.Kill(pid, unix.SIGINT) 82 | } 83 | 84 | // RestoreForegroundGroup places the group for this process back in the foreground. 85 | func RestoreForegroundGroup() { 86 | if group == ForegroundGroup() { 87 | return 88 | } 89 | 90 | SetForegroundGroup(group) 91 | } 92 | 93 | // SetForegroundGroup sets the terminal's foregeound group to g. 94 | func SetForegroundGroup(g int) { 95 | err := unix.IoctlSetPointerInt(terminal, unix.TIOCSPGRP, g) 96 | if err != nil { 97 | println(err.Error()) 98 | } 99 | } 100 | 101 | // Stop sends a SIGSTOP to the process ID pid. 102 | func Stop(pid int) { 103 | _ = unix.Kill(pid, unix.SIGSTOP) 104 | } 105 | 106 | // SysProcAttr returns the appropriate *unix.SysProcAttr given the group ID 107 | // and if this is for the foreground group. 108 | func SysProcAttr(foreground bool, group int) *unix.SysProcAttr { 109 | sys := &unix.SysProcAttr{Foreground: foreground, Setpgid: true} 110 | 111 | if group == 0 { 112 | sys.Ctty = terminal 113 | } else { 114 | sys.Pgid = group 115 | } 116 | 117 | return sys 118 | } 119 | 120 | // Terminate sends a SIGTERM to the process ID pid. 121 | func Terminate(pid int) { 122 | _ = unix.Kill(pid, unix.SIGTERM) 123 | } 124 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/michaelmacinnis/oh/internal/common" 13 | "github.com/michaelmacinnis/oh/internal/common/interface/cell" 14 | "github.com/michaelmacinnis/oh/internal/common/type/list" 15 | "github.com/michaelmacinnis/oh/internal/common/type/pair" 16 | "github.com/michaelmacinnis/oh/internal/common/type/str" 17 | "github.com/michaelmacinnis/oh/internal/common/type/sym" 18 | "github.com/michaelmacinnis/oh/internal/engine" 19 | "github.com/michaelmacinnis/oh/internal/reader" 20 | "github.com/michaelmacinnis/oh/internal/system/cache" 21 | "github.com/michaelmacinnis/oh/internal/system/history" 22 | "github.com/michaelmacinnis/oh/internal/system/job" 23 | "github.com/michaelmacinnis/oh/internal/system/options" 24 | "github.com/michaelmacinnis/oh/internal/system/process" 25 | "github.com/peterh/liner" 26 | ) 27 | 28 | //nolint:gochecknoglobals 29 | var ( 30 | pathListSeparator = string(os.PathListSeparator) 31 | pathSeparator = string(os.PathSeparator) 32 | ) 33 | 34 | func clean(s string) string { 35 | if s == "." || s == pathSeparator+"." { 36 | return s 37 | } 38 | 39 | head, tail := split(s) 40 | if tail == s { 41 | head, tail = tail, head 42 | } 43 | 44 | return filepath.Clean(head) + tail 45 | } 46 | 47 | func command() bool { 48 | if options.Command() == "" { 49 | return false 50 | } 51 | 52 | r := reader.New(os.Args[0]) 53 | 54 | c, err := r.Scan(options.Command() + "\n") 55 | if err != nil { 56 | println("problem parsing command:", err.Error()) 57 | os.Exit(1) 58 | } 59 | 60 | if c == nil { 61 | println("incomplete command") 62 | os.Exit(1) 63 | } 64 | 65 | engine.Evaluate(job.New(process.Group()), c) 66 | 67 | return true 68 | } 69 | 70 | func completer(j **job.T, r **reader.T) func(s string, n int) (h string, cs []string, t string) { 71 | return func(s string, n int) (h string, cs []string, t string) { 72 | h = s[:n] 73 | t = s[n:] 74 | 75 | last := strings.LastIndexAny(h, " ()") 76 | completing := h[last+1:] 77 | 78 | defer func() { 79 | r := recover() 80 | if r == nil { 81 | return 82 | } 83 | 84 | cs = []string{} 85 | }() 86 | 87 | lc := (*r).Lexer().Copy() 88 | 89 | lc.Scan(h) 90 | 91 | lp := (*r).Parser().Copy(func(_ cell.I) {}, lc.Token) 92 | 93 | _ = lp.Parse() 94 | 95 | cs = lc.Expected() 96 | if len(cs) != 0 { 97 | return 98 | } 99 | 100 | // Ensure line == prefix + completing + tail 101 | prefix := h[0 : len(h)-len(completing)] 102 | 103 | cwd := engine.Resolve("PWD") 104 | home := engine.Resolve("HOME") 105 | 106 | cmd := lp.Current() 107 | if cmd == pair.Null || last < 0 || h[last:last+1] == "(" { 108 | if completing == "" { 109 | cs = []string{" "} 110 | 111 | return 112 | } 113 | 114 | cmd := list.New(sym.New("complete"), sym.New(completing)) 115 | v, _ := engine.System(*j, cmd) 116 | 117 | process.RestoreForegroundGroup() 118 | 119 | for c := pair.Cdr(v); c != pair.Null; c = pair.Cdr(c) { 120 | cs = append(cs, common.String(pair.Car(c))) 121 | } 122 | } else { 123 | cmd := list.Append(list.New(list.Array(cmd)...), sym.New(completing)) 124 | v, _ := engine.System(*j, pair.Cons(sym.New("complete"), cmd)) 125 | 126 | process.RestoreForegroundGroup() 127 | 128 | cs = files(cache.Files, cwd, home, engine.Resolve("PWD"), completing) 129 | 130 | cfname := common.String(pair.Car(v)) 131 | if cfname != "_minimal" && cfname != "_filedir_xspec" { 132 | for c := pair.Cdr(v); c != pair.Null; c = pair.Cdr(c) { 133 | cs = append(cs, common.String(pair.Car(c))) 134 | } 135 | } 136 | } 137 | 138 | if len(cs) == 0 { 139 | return prefix, []string{completing}, t 140 | } 141 | 142 | unique := make(map[string]bool) 143 | for _, completion := range cs { 144 | unique[completion] = true 145 | } 146 | 147 | cs = make([]string, 0, len(unique)) 148 | for completion := range unique { 149 | cs = append(cs, completion) 150 | } 151 | 152 | sort.Strings(cs) 153 | 154 | return prefix, cs, t 155 | } 156 | } 157 | 158 | func directories(s string) []string { 159 | dirs := []string{} 160 | 161 | for _, dir := range strings.Split(s, pathListSeparator) { 162 | if dir == "" { 163 | dir = "." 164 | } else { 165 | dir = filepath.Clean(dir) 166 | } 167 | 168 | stat, err := os.Stat(dir) 169 | if err != nil || !stat.IsDir() { 170 | continue 171 | } 172 | 173 | dirs = append(dirs, dir) 174 | } 175 | 176 | return dirs 177 | } 178 | 179 | func expand(cwd, home, candidate string) (string, bool) { 180 | dotdir := false 181 | prefix := candidate + " " 182 | 183 | switch { 184 | case candidate == "": 185 | // Leave it as is. 186 | 187 | case prefix[0:2] == "./" || prefix[0:3] == "../": 188 | candidate = join(cwd, candidate) 189 | dotdir = true 190 | 191 | case prefix[0:1] == "~": 192 | candidate = join(home, candidate[1:]) 193 | 194 | default: 195 | candidate = clean(candidate) 196 | } 197 | 198 | return candidate, dotdir 199 | } 200 | 201 | func files(cached func(string) []string, cwd, home, paths, word string) []string { 202 | candidate, dotdir := expand(cwd, home, word) 203 | 204 | candidates := []string{candidate} 205 | if !path.IsAbs(candidate) && !dotdir { 206 | candidates = directories(paths) 207 | for k, v := range candidates { 208 | candidates[k] = v + pathSeparator + candidate 209 | } 210 | } 211 | 212 | return matches(cached, candidates, word) 213 | } 214 | 215 | func interactive() bool { 216 | if !options.Interactive() { 217 | return false 218 | } 219 | 220 | go cache.Populate(engine.Resolve("PATH")) 221 | 222 | name := options.Args()[0] 223 | 224 | err := process.BecomeForegroundGroup() 225 | if err != nil { 226 | println(err.Error()) 227 | 228 | return false 229 | } 230 | 231 | // We assume the terminal starts in cooked mode. 232 | cooked, err := liner.TerminalMode() 233 | if err != nil { 234 | println(err.Error()) 235 | 236 | return false 237 | } 238 | 239 | // Restore terminal state when we exit. 240 | defer func() { 241 | err := cooked.ApplyMode() 242 | if err != nil { 243 | println(err.Error()) 244 | } 245 | }() 246 | 247 | cli := liner.NewLiner() 248 | 249 | cli.SetCtrlCAborts(true) 250 | 251 | uncooked, err := liner.TerminalMode() 252 | if err != nil { 253 | println(err.Error()) 254 | 255 | return false 256 | } 257 | 258 | err = history.Load(cli.ReadHistory) 259 | if err != nil { 260 | println(err.Error()) 261 | } 262 | 263 | defer func() { 264 | err = history.Save(cli.WriteHistory) 265 | if err != nil { 266 | println(err.Error()) 267 | } 268 | 269 | _, _ = os.Stdout.Write([]byte{'\n'}) 270 | }() 271 | 272 | err = repl(cli, cooked, uncooked, name) 273 | if !errors.Is(err, io.EOF) { 274 | println(err.Error()) 275 | } 276 | 277 | return true 278 | } 279 | 280 | func join(s ...string) string { 281 | last := len(s) - 1 282 | head, tail := split(s[last]) 283 | s[last] = head 284 | 285 | return filepath.Join(s...) + tail 286 | } 287 | 288 | func matches(cached func(string) []string, candidates []string, word string) []string { 289 | completions := []string{} 290 | 291 | for _, candidate := range candidates { 292 | dirname, basename := filepath.Split(candidate) 293 | 294 | if skip(dirname, basename) { 295 | continue 296 | } 297 | 298 | for _, p := range cached(dirname) { 299 | if candidate != pathSeparator && len(basename) == 0 { 300 | suffix := strings.TrimPrefix(p, dirname) 301 | if strings.HasPrefix(suffix, ".") { 302 | continue 303 | } 304 | } else if !strings.HasPrefix(p, candidate) { 305 | continue 306 | } 307 | 308 | if len(candidate) > len(p) { 309 | return nil 310 | } 311 | 312 | s := strings.Index(p, candidate) + len(candidate) 313 | completion := word + p[s:] 314 | completions = append(completions, completion) 315 | } 316 | } 317 | 318 | return completions 319 | } 320 | 321 | func skip(dirname, basename string) bool { 322 | stat, err := os.Stat(dirname) 323 | if err != nil { 324 | return true 325 | } else if len(basename) == 0 && !stat.IsDir() { 326 | return true 327 | } 328 | 329 | return false 330 | } 331 | 332 | func split(s string) (head, tail string) { 333 | head = s 334 | tail = "" 335 | 336 | index := strings.LastIndex(s, pathSeparator) 337 | if index > -1 { 338 | head = s[:index] 339 | tail = s[index:] 340 | } 341 | 342 | return 343 | } 344 | 345 | func repl(cli *liner.State, cooked, uncooked liner.ModeApplier, name string) error { 346 | j := job.New(0) 347 | r := reader.New(name) 348 | 349 | cli.SetWordCompleter(completer(&j, &r)) 350 | cli.SetTabCompletionStyle(liner.TabPrints) 351 | 352 | continued := str.New(" ") 353 | initial := str.New(": ") 354 | suffix := initial 355 | 356 | for { 357 | v, _ := engine.System(j, list.New(sym.New("prompt"), suffix)) 358 | p := common.String(v) 359 | 360 | err := uncooked.ApplyMode() 361 | if err != nil { 362 | return err 363 | } 364 | 365 | line, err := cli.Prompt(p) 366 | if err != nil { 367 | if errors.Is(err, liner.ErrPromptAborted) { 368 | r.Close() 369 | r = reader.New(name) 370 | suffix = initial 371 | 372 | continue 373 | } else { 374 | return err 375 | } 376 | } 377 | 378 | err = cooked.ApplyMode() 379 | if err != nil { 380 | return err 381 | } 382 | 383 | if line == "" { 384 | continue 385 | } 386 | 387 | cli.AppendHistory(line) 388 | j.Append(line) 389 | 390 | suffix = continued 391 | 392 | c, err := r.Scan(line + "\n") 393 | if err != nil { 394 | println(err.Error()) 395 | 396 | r.Close() 397 | 398 | suffix = initial 399 | r = reader.New(name) 400 | 401 | continue 402 | } 403 | 404 | if c != nil { 405 | engine.Evaluate(j, c) 406 | 407 | process.RestoreForegroundGroup() 408 | 409 | r.Close() 410 | 411 | suffix = initial 412 | r = reader.New(name) 413 | 414 | j = job.New(0) 415 | } 416 | } 417 | } 418 | 419 | func main() { 420 | options.Parse() 421 | 422 | if options.Version() { 423 | // TODO: Get fancier later with where the version is defined. 424 | println("oh v0.8.3") 425 | return 426 | } 427 | 428 | engine.Boot(options.Script(), options.Args()) 429 | 430 | if !command() && !interactive() { 431 | println("unexpected error") 432 | } 433 | } 434 | 435 | //go:generate ./oh bin/test.oh 436 | //go:generate ./oh bin/doc.oh manual ../doc/manual.md 437 | //go:generate ./oh bin/type-common.oh internal/common/type/chn 438 | //go:generate ./oh bin/type-common.oh internal/common/type/env 439 | //go:generate ./oh bin/type-common.oh internal/common/type/num 440 | //go:generate ./oh bin/type-common.oh internal/common/type/obj 441 | //go:generate ./oh bin/type-common.oh internal/common/type/pair 442 | //go:generate ./oh bin/type-common.oh internal/common/type/pipe 443 | //go:generate ./oh bin/type-common.oh internal/common/type/status 444 | //go:generate ./oh bin/type-common.oh internal/common/type/str 445 | --------------------------------------------------------------------------------