├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.fish ├── go.mod ├── go.sum ├── main.go └── translate ├── err.go ├── translate.go └── translate_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: stable 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | babelfish 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bouke van der Bijl 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babelfish 2 | 3 | Translate bash scripts to [fish](https://fishshell.com). 4 | 5 | ## Why? 6 | 7 | Because I got annoyed by having to use [fish-foreign-env](https://github.com/oh-my-fish/plugin-foreign-env) or [bass](https://github.com/edc/bass), which are slow, since they create multiple bash processes. With this program I can translate bash scripts to fish, and run them directly in fish. 8 | 9 | ## But how? 10 | 11 | `babelfish` parses the script using [mvdan.cc/sh](https://github.com/mvdan/sh), and then translates bash expressions to the equivalent fish code. That's it! You can find the code that walks the AST and emits fish code [here](https://github.com/bouk/babelfish/blob/master/translate/translate.go). 12 | 13 | ## Install 14 | 15 | If you have Homebrew, just run: 16 | 17 | ```shell 18 | brew install babelfish 19 | ``` 20 | 21 | Else: 22 | 23 | ```shell 24 | go install bou.ke/babelfish@latest 25 | ``` 26 | 27 | ## Example 28 | 29 | ```sh 30 | # Pass some code on stdin to translate it 31 | $ echo 'f() { export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket); local cool=yep; }' | babelfish 32 | function f 33 | set -gx SSH_AUTH_SOCK (gpgconf --list-dirs agent-ssh-socket | string collect; or echo) 34 | set -l cool 'yep' 35 | end 36 | # Pass the result to source to load it into fish 37 | $ echo 'echo Nice to meet you user $UID' | babelfish | source 38 | Nice to meet you user 502 39 | # Or install the shell hook! 40 | $ source babel.fish 41 | $ source chruby.sh 42 | $ chruby 43 | ruby-2.5 44 | ruby-2.6 45 | ruby-2.7 46 | ``` 47 | 48 | ## To do 49 | 50 | Probably still a lot. There's a couple variables like `$BASH_SOURCE` that aren't translated, and not all arithmetic expressions are implemented either. Pull requests and issues welcome! 51 | -------------------------------------------------------------------------------- /babel.fish: -------------------------------------------------------------------------------- 1 | # We are using -S to ensure the scope is correct 2 | function _babelfish_source -S 3 | if test "$argv[1]" = '-' || string match -q '*.fish' "$argv[1]" || test -z "$argv[1]" 4 | builtin source $argv 5 | else 6 | babelfish < $argv[1] | builtin source 7 | end 8 | end 9 | 10 | function source -S 11 | _babelfish_source $argv 12 | end 13 | 14 | function . -S 15 | _babelfish_source $argv 16 | end 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bou.ke/babelfish 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.9 7 | mvdan.cc/sh/v3 v3.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 2 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 3 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 5 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 6 | github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= 7 | mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= 8 | mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main // import "bou.ke/babelfish" 2 | 3 | import ( 4 | "bou.ke/babelfish/translate" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "mvdan.cc/sh/v3/syntax" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type Options struct { 14 | Dump bool 15 | } 16 | 17 | func perform(name string, in io.Reader) error { 18 | out := os.Stdout 19 | errOut := os.Stderr 20 | p := syntax.NewParser(syntax.KeepComments(true), syntax.Variant(syntax.LangBash)) 21 | output, err := p.Parse(in, name) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | t := translate.NewTranslator() 27 | 28 | loc := os.Args[0] 29 | // If the file path is relative, make it absolute 30 | if len(loc) > 0 && loc[0] == '.' { 31 | if wd, err := os.Getwd(); err == nil { 32 | loc = filepath.Join(wd, loc) 33 | } 34 | } 35 | t.BabelfishLocation(loc) 36 | 37 | err = t.File(output) 38 | if err, _ := err.(*translate.UnsupportedError); err != nil { 39 | syntax.NewPrinter().Print(errOut, err.Node) 40 | fmt.Fprintln(errOut) 41 | syntax.DebugPrint(errOut, err.Node) 42 | fmt.Fprintln(errOut) 43 | } 44 | if err == nil { 45 | _, err = t.WriteTo(out) 46 | } 47 | return err 48 | } 49 | 50 | func do() error { 51 | var o Options 52 | flag.BoolVar(&o.Dump, "dump", false, "Dump the AST") 53 | flag.Parse() 54 | 55 | f := os.Stdin 56 | if o.Dump { 57 | p := syntax.NewParser(syntax.KeepComments(true), syntax.Variant(syntax.LangBash)) 58 | output, err := p.Parse(f, f.Name()) 59 | if err != nil { 60 | return err 61 | } 62 | syntax.DebugPrint(os.Stderr, output) 63 | fmt.Fprintln(os.Stderr) 64 | return nil 65 | } 66 | return perform(f.Name(), f) 67 | } 68 | 69 | func main() { 70 | if err := do(); err != nil { 71 | fmt.Println(err) 72 | os.Exit(1) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /translate/err.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "fmt" 5 | "mvdan.cc/sh/v3/syntax" 6 | ) 7 | 8 | type UnsupportedError struct { 9 | Node syntax.Node 10 | } 11 | 12 | func (u *UnsupportedError) Error() string { 13 | return fmt.Sprintf("unsupported: %#v", u.Node) 14 | } 15 | 16 | func unsupported(n syntax.Node) { 17 | panic(&UnsupportedError{n}) 18 | } 19 | -------------------------------------------------------------------------------- /translate/translate.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strings" 9 | 10 | "mvdan.cc/sh/v3/pattern" 11 | "mvdan.cc/sh/v3/syntax" 12 | ) 13 | 14 | // Translator 15 | // 16 | // The translation functions internally panic, which gets caught by File 17 | type Translator struct { 18 | buf *bytes.Buffer 19 | indentLevel int 20 | babelFishLocation string 21 | } 22 | 23 | func NewTranslator() *Translator { 24 | return &Translator{ 25 | buf: &bytes.Buffer{}, 26 | } 27 | } 28 | 29 | func (t *Translator) BabelfishLocation(loc string) { 30 | t.babelFishLocation = loc 31 | } 32 | 33 | func (t *Translator) WriteTo(w io.Writer) (int64, error) { 34 | return t.buf.WriteTo(w) 35 | } 36 | 37 | func (t *Translator) File(f *syntax.File) (err error) { 38 | // So I don't have to write if err all the time 39 | defer func() { 40 | if v := recover(); v != nil { 41 | if perr, ok := v.(*UnsupportedError); ok { 42 | err = perr 43 | return 44 | } 45 | panic(v) 46 | } 47 | }() 48 | 49 | for i, stmt := range f.Stmts { 50 | t.stmt(stmt) 51 | t.nl() 52 | 53 | isLast := i == len(f.Stmts)-1 54 | _, ok := stmt.Cmd.(*syntax.FuncDecl) 55 | 56 | if ok && !isLast { 57 | currentEnd := stmt.End() 58 | nextPos := f.Stmts[i+1].Pos() 59 | 60 | if currentEnd.Line() < nextPos.Line()-1 { 61 | t.nl() 62 | } 63 | } 64 | } 65 | 66 | for _, comment := range f.Last { 67 | t.comment(&comment) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (t *Translator) stmt(s *syntax.Stmt) { 74 | if s.Background || s.Coprocess { 75 | unsupported(s) 76 | } 77 | 78 | for _, comment := range s.Comments { 79 | t.comment(&comment) 80 | } 81 | 82 | if s.Negated { 83 | t.str("! ") 84 | } 85 | t.command(s.Cmd) 86 | for _, r := range s.Redirs { 87 | t.str(" ") 88 | 89 | if r.N != nil { 90 | t.str(r.N.Value) 91 | } 92 | switch r.Op { 93 | case syntax.RdrInOut, syntax.RdrIn, syntax.RdrOut, syntax.AppOut, syntax.DplIn, syntax.DplOut: 94 | t.str(r.Op.String()) 95 | t.word(r.Word, false) 96 | case syntax.Hdoc: 97 | t.str("<(echo ") 98 | t.word(r.Hdoc, true) 99 | t.str("| psub)") 100 | case syntax.WordHdoc: 101 | t.str("<(echo ") 102 | t.word(r.Word, true) 103 | t.str("| psub)") 104 | default: 105 | unsupported(s) 106 | } 107 | } 108 | } 109 | 110 | type arithmReturn int 111 | 112 | const ( 113 | arithmReturnValue arithmReturn = iota 114 | arithmReturnStatus 115 | ) 116 | 117 | func (t *Translator) arithmExpr(e syntax.ArithmExpr, returnValue arithmReturn) { 118 | switch e := e.(type) { 119 | case *syntax.BinaryArithm: 120 | switch e.Op { 121 | case syntax.Eql: 122 | switch returnValue { 123 | case arithmReturnValue: 124 | t.str("(") 125 | } 126 | t.str("test ") 127 | t.arithmExpr(e.X, arithmReturnValue) 128 | t.str(" -eq ") 129 | t.arithmExpr(e.Y, arithmReturnValue) 130 | switch returnValue { 131 | case arithmReturnValue: 132 | t.str("; and echo 1; or echo 0)") 133 | } 134 | case syntax.Neq: 135 | switch returnValue { 136 | case arithmReturnValue: 137 | t.str("(") 138 | } 139 | t.str("test ") 140 | t.arithmExpr(e.X, arithmReturnValue) 141 | t.str(" -ne ") 142 | t.arithmExpr(e.Y, arithmReturnValue) 143 | switch returnValue { 144 | case arithmReturnValue: 145 | t.str("; and echo 1; or echo 0)") 146 | } 147 | default: 148 | unsupported(e) 149 | } 150 | case *syntax.UnaryArithm: 151 | unsupported(e) 152 | case *syntax.ParenArithm: 153 | unsupported(e) 154 | case *syntax.Word: 155 | l, ok := lit(e) 156 | if !ok { 157 | unsupported(e) 158 | } 159 | 160 | switch returnValue { 161 | case arithmReturnStatus: 162 | t.str("test ") 163 | } 164 | if syntax.ValidName(l) { 165 | if expr, ok := literalVariables[l]; ok { 166 | t.str(expr) 167 | } else { 168 | t.printf(`"$%s"`, l) 169 | } 170 | } else { 171 | t.str(l) 172 | } 173 | switch returnValue { 174 | case arithmReturnStatus: 175 | t.str(" != 0") 176 | } 177 | default: 178 | unsupported(e) 179 | } 180 | } 181 | 182 | func (t *Translator) command(c syntax.Command) { 183 | switch c := c.(type) { 184 | case *syntax.ArithmCmd: 185 | t.arithmExpr(c.X, arithmReturnStatus) 186 | case *syntax.BinaryCmd: 187 | t.binaryCmd(c) 188 | case *syntax.Block: 189 | // TODO: Maybe need begin/end here, sometimes? Not for function 190 | t.body(c.Stmts...) 191 | case *syntax.CallExpr: 192 | t.callExpr(c) 193 | case *syntax.CaseClause: 194 | t.caseClause(c) 195 | case *syntax.CoprocClause: 196 | unsupported(c) 197 | case *syntax.DeclClause: 198 | t.declClause(c) 199 | case *syntax.ForClause: 200 | if c.Select { 201 | unsupported(c) 202 | } 203 | t.str("for ") 204 | 205 | switch l := c.Loop.(type) { 206 | case *syntax.WordIter: 207 | t.printf("%s", l.Name.Value) 208 | 209 | if l.InPos.IsValid() { 210 | t.str(" in") 211 | for _, w := range l.Items { 212 | t.str(" ") 213 | t.word(w, false) 214 | } 215 | } else { 216 | t.str(" in $argv") 217 | } 218 | 219 | default: 220 | unsupported(c) 221 | } 222 | 223 | t.indent() 224 | t.body(c.Do...) 225 | t.outdent() 226 | t.str("end") 227 | case *syntax.FuncDecl: 228 | t.printf("function %s", c.Name.Value) 229 | t.indent() 230 | t.stmt(c.Body) 231 | t.outdent() 232 | t.str("end") 233 | case *syntax.IfClause: 234 | t.ifClause(c, false) 235 | case *syntax.LetClause: 236 | unsupported(c) 237 | case *syntax.Subshell: 238 | t.str("fish -c ") 239 | t.capture(func() { 240 | t.stmts(c.Stmts...) 241 | }) 242 | case *syntax.TestClause: 243 | t.testClause(c) 244 | case *syntax.TimeClause: 245 | t.str("time ") 246 | t.stmt(c.Stmt) 247 | case *syntax.WhileClause: 248 | t.str("while ") 249 | if c.Until { 250 | t.str("not ") 251 | } 252 | t.stmts(c.Cond...) 253 | t.indent() 254 | t.body(c.Do...) 255 | t.outdent() 256 | t.str("end") 257 | default: 258 | unsupported(c) 259 | } 260 | } 261 | 262 | func (t *Translator) caseClause(c *syntax.CaseClause) { 263 | t.str("switch ") 264 | t.word(c.Word, true) 265 | t.nl() 266 | for _, item := range c.Items { 267 | if item.Op != syntax.Break { 268 | unsupported(item) 269 | } 270 | t.str("case") 271 | for _, pat := range item.Patterns { 272 | t.str(" ") 273 | t.word(pat, true) 274 | } 275 | t.indent() 276 | t.body(item.Stmts...) 277 | t.outdent() 278 | } 279 | t.str("end") 280 | } 281 | 282 | func (t *Translator) testClause(c *syntax.TestClause) { 283 | t.str("test ") 284 | t.testExpr(c.X) 285 | } 286 | 287 | func (t *Translator) testExpr(e syntax.TestExpr) { 288 | switch e := e.(type) { 289 | case *syntax.BinaryTest: 290 | t.testExpr(e.X) 291 | switch e.Op { 292 | case syntax.AndTest: 293 | t.str(" && test ") 294 | case syntax.OrTest: 295 | t.str(" || test ") 296 | case syntax.TsMatch: 297 | t.str(" = ") 298 | case syntax.TsNoMatch: 299 | t.str(" != ") 300 | case syntax.TsEql, 301 | syntax.TsNeq, 302 | syntax.TsLeq, 303 | syntax.TsGeq, 304 | syntax.TsLss, 305 | syntax.TsGtr: 306 | t.printf(" %s ", e.Op) 307 | default: 308 | unsupported(e) 309 | } 310 | t.testExpr(e.Y) 311 | case *syntax.ParenTest: 312 | t.str(`\( `) 313 | t.testExpr(e.X) 314 | t.str(` \)`) 315 | case *syntax.UnaryTest: 316 | switch e.Op { 317 | case syntax.TsExists, 318 | syntax.TsRegFile, 319 | syntax.TsDirect, 320 | syntax.TsCharSp, 321 | syntax.TsBlckSp, 322 | syntax.TsNmPipe, 323 | syntax.TsSocket, 324 | syntax.TsSmbLink, 325 | syntax.TsSticky, 326 | syntax.TsGIDSet, 327 | syntax.TsUIDSet, 328 | syntax.TsGrpOwn, 329 | syntax.TsUsrOwn, 330 | syntax.TsRead, 331 | syntax.TsWrite, 332 | syntax.TsExec, 333 | syntax.TsNoEmpty, 334 | syntax.TsFdTerm, 335 | 336 | syntax.TsEmpStr, 337 | syntax.TsNempStr, 338 | 339 | syntax.TsNot: 340 | t.printf("%s ", e.Op) 341 | default: 342 | unsupported(e) 343 | } 344 | t.testExpr(e.X) 345 | case *syntax.Word: 346 | t.word(e, true) 347 | } 348 | } 349 | 350 | func (t *Translator) ifClause(i *syntax.IfClause, elif bool) { 351 | if elif { 352 | t.str("else if ") 353 | } else { 354 | t.str("if ") 355 | } 356 | t.stmts(i.Cond...) 357 | t.indent() 358 | t.body(i.Then...) 359 | t.outdent() 360 | 361 | el := i.Else 362 | if el != nil && el.ThenPos.IsValid() { 363 | t.ifClause(el, true) 364 | return 365 | } 366 | 367 | if el == nil { 368 | // comments 369 | } else { 370 | t.str("else") 371 | t.indent() 372 | t.body(el.Then...) 373 | t.outdent() 374 | } 375 | 376 | t.str("end") 377 | } 378 | 379 | func (t *Translator) stmts(s ...*syntax.Stmt) { 380 | for i, s := range s { 381 | if i > 0 { 382 | t.str("; ") 383 | } 384 | t.stmt(s) 385 | } 386 | } 387 | 388 | func (t *Translator) body(s ...*syntax.Stmt) { 389 | for i, s := range s { 390 | if i > 0 { 391 | t.nl() 392 | } 393 | t.stmt(s) 394 | } 395 | } 396 | 397 | func (t *Translator) binaryCmd(c *syntax.BinaryCmd) { 398 | switch c.Op { 399 | case syntax.AndStmt: 400 | t.stmt(c.X) 401 | t.str(" && ") 402 | t.stmt(c.Y) 403 | return 404 | case syntax.OrStmt: 405 | t.stmt(c.X) 406 | t.str(" || ") 407 | t.stmt(c.Y) 408 | return 409 | case syntax.Pipe: 410 | t.stmt(c.X) 411 | t.str(" | ") 412 | t.stmt(c.Y) 413 | return 414 | case syntax.PipeAll: 415 | unsupported(c) 416 | } 417 | } 418 | 419 | func (t *Translator) assign(prefix string, a *syntax.Assign) { 420 | if a.Append { 421 | prefix += " -a" 422 | } 423 | switch { 424 | case a.Naked: 425 | t.printf("set%s %s ", prefix, a.Name.Value) 426 | t.printf("$%s", a.Name.Value) 427 | case a.Array != nil: 428 | t.printf("set%s %s", prefix, a.Name.Value) 429 | for _, el := range a.Array.Elems { 430 | if el.Index != nil || el.Value == nil { 431 | unsupported(a) 432 | } 433 | t.str(" ") 434 | t.word(el.Value, false) 435 | } 436 | case a.Value != nil: 437 | t.printf("set%s %s ", prefix, a.Name.Value) 438 | t.word(a.Value, true) 439 | case a.Index != nil: 440 | unsupported(a) 441 | } 442 | } 443 | 444 | func (t *Translator) callExpr(c *syntax.CallExpr) { 445 | if len(c.Args) == 0 { 446 | // assignment 447 | for n, a := range c.Assigns { 448 | if n > 0 { 449 | t.str("; ") 450 | } 451 | t.assign("", a) 452 | } 453 | } else { 454 | // call 455 | if len(c.Assigns) > 0 { 456 | for _, a := range c.Assigns { 457 | t.printf("%s=", a.Name.Value) 458 | if a.Value != nil { 459 | t.word(a.Value, true) 460 | } 461 | t.str(" ") 462 | } 463 | } 464 | 465 | first := c.Args[0] 466 | l, _ := lit(first) 467 | switch l { 468 | case "shift": 469 | t.str("set -e argv[1]") 470 | case "unset": 471 | isFirst := true 472 | unsetFunc := false 473 | for _, a := range c.Args[1:] { 474 | aStr, _ := lit(a) 475 | if aStr == "-f" { 476 | unsetFunc = true 477 | continue 478 | } else if aStr == "-v" { 479 | unsetFunc = false 480 | continue 481 | } 482 | if !isFirst { 483 | t.str("; ") 484 | } 485 | isFirst = false 486 | if unsetFunc { 487 | t.str("functions -e ") 488 | } else { 489 | t.str("set -e ") 490 | } 491 | t.word(a, false) 492 | } 493 | return 494 | case "hash": 495 | t.str("true") 496 | return 497 | case "source", ".": 498 | if len(c.Args) == 2 && t.babelFishLocation != "" { 499 | t.str(t.babelFishLocation) 500 | t.str(" < ") 501 | t.word(c.Args[1], false) 502 | t.str(" | source") 503 | return 504 | } 505 | fallthrough 506 | default: 507 | t.word(first, false) 508 | } 509 | 510 | for _, a := range c.Args[1:] { 511 | t.str(" ") 512 | t.word(a, false) 513 | } 514 | } 515 | } 516 | 517 | func (t *Translator) declClause(c *syntax.DeclClause) { 518 | var prefix string 519 | if c.Variant != nil { 520 | switch c.Variant.Value { 521 | case "export": 522 | prefix = " -gx" 523 | case "local": 524 | prefix = " -l" 525 | default: 526 | unsupported(c) 527 | } 528 | } 529 | 530 | for i, a := range c.Args { 531 | if a.Name == nil { 532 | unsupported(c) 533 | } 534 | if i > 0 { 535 | t.str("; ") 536 | } 537 | t.assign(prefix, a) 538 | } 539 | } 540 | 541 | func (t *Translator) word(w *syntax.Word, mustQuote bool) { 542 | if w == nil { 543 | t.str(`''`) 544 | return 545 | } 546 | 547 | quote := mustQuote 548 | for _, part := range w.Parts { 549 | t.wordPart(part, quote) 550 | } 551 | } 552 | 553 | // wordPart spits out a piece of a Word. The wordparts are placed next to each other, so that they are concatenated into one. 554 | // NOTE: This 'concatentation' is actually a cartesian product. 555 | // This means that every part *needs* to return a list with exactly one item. 556 | // For commands, this means they need to return with just one newline at the end. This means we might need to do something like: 557 | // (begin; ;echo;end | string collect) 558 | // To ensure there's always one result. 559 | // 560 | // quote specifies whether this needs to be quoted. This is done so variables and command substitution get expanded. 561 | func (t *Translator) wordPart(wp syntax.WordPart, quoted bool) { 562 | switch wp := wp.(type) { 563 | case *syntax.Lit: 564 | s := wp.Value 565 | if quoted { 566 | s = unescape(s) 567 | t.escapedString(s) 568 | } else { 569 | t.str(s) 570 | } 571 | case *syntax.SglQuoted: 572 | t.escapedString(wp.Value) 573 | case *syntax.DblQuoted: 574 | if len(wp.Parts) == 0 { 575 | t.str(`''`) 576 | } 577 | for _, part := range wp.Parts { 578 | t.wordPart(part, true) 579 | } 580 | case *syntax.ParamExp: 581 | t.paramExp(wp, quoted) 582 | case *syntax.CmdSubst: 583 | // Need to ensure there's one element returned from the subst 584 | t.str("(") 585 | t.stmts(wp.Stmts...) 586 | if quoted { 587 | t.str(" | string collect; or echo") 588 | } 589 | t.str(")") 590 | case *syntax.ArithmExp: 591 | t.arithmExpr(wp.X, arithmReturnValue) 592 | case *syntax.ProcSubst: 593 | t.str("(") 594 | t.stmts(wp.Stmts...) 595 | switch wp.Op { 596 | case syntax.CmdIn: 597 | t.str(" | psub") 598 | case syntax.CmdOut: 599 | unsupported(wp) 600 | } 601 | t.str(")") 602 | case *syntax.ExtGlob: 603 | unsupported(wp) 604 | default: 605 | unsupported(wp) 606 | } 607 | } 608 | 609 | var specialVariables = map[string]string{ 610 | //"!": "%last", % variables are weird 611 | "?": "status", 612 | "$": "fish_pid", 613 | "BASH_PID": "fish_pid", 614 | "*": `argv`, // always quote 615 | "@": "argv", 616 | "HOSTNAME": "hostname", 617 | } 618 | 619 | // http://tldp.org/LDP/abs/html/internalvariables.html 620 | var literalVariables = map[string]string{ 621 | "UID": "(id -ru)", 622 | "EUID": "(id -u)", 623 | "GROUPS": "(id -G | string split ' ')", 624 | } 625 | 626 | var argvRe = regexp.MustCompile(`^[0-9]+$`) 627 | 628 | func (t *Translator) paramExp(p *syntax.ParamExp, quoted bool) { 629 | param := p.Param.Value 630 | if expr, ok := literalVariables[param]; ok { 631 | t.str(expr) 632 | return 633 | } 634 | if argvRe.MatchString(param) { 635 | t.printf(`$argv[%s]`, param) 636 | return 637 | } 638 | 639 | if spec, ok := specialVariables[param]; ok { 640 | // 🤷 641 | if param == "*" { 642 | quoted = true 643 | } 644 | param = spec 645 | } 646 | switch { 647 | case p.Excl: // ${!a} 648 | unsupported(p) 649 | case p.Length: // ${#a} 650 | index := p.Index 651 | switch p.Param.Value { 652 | case "@", "*": 653 | index = &syntax.Word{Parts: []syntax.WordPart{p.Param}} 654 | } 655 | if index != nil { 656 | if word, ok := index.(*syntax.Word); ok { 657 | switch word.Lit() { 658 | case "@", "*": 659 | t.printf("(count $%s)", param) 660 | return 661 | } 662 | } 663 | unsupported(p) 664 | } 665 | t.printf(`(string length "$%s")`, param) 666 | case p.Index != nil: // ${a[i]}, ${a["k"]} 667 | if word, ok := p.Index.(*syntax.Word); ok { 668 | switch word.Lit() { 669 | case "@": 670 | t.printf(`$%s`, param) 671 | return 672 | case "*": 673 | t.printf(`"$%s"`, param) 674 | return 675 | } 676 | } 677 | unsupported(p) 678 | case p.Width: // ${%a} 679 | unsupported(p) 680 | case p.Slice != nil: // ${a:x:y} 681 | unsupported(p) 682 | case p.Repl != nil: // ${a/x/y} 683 | t.str("(string replace ") 684 | if p.Repl.All { 685 | t.str("--all ") 686 | } 687 | t.word(p.Repl.Orig, true) 688 | t.str(" ") 689 | t.word(p.Repl.With, true) 690 | t.printf(` "$%s")`, param) 691 | case p.Names != 0: // ${!prefix*} or ${!prefix@} 692 | unsupported(p) 693 | case p.Exp != nil: 694 | // TODO: should probably allow lists to be expanded here 695 | switch op := p.Exp.Op; op { 696 | case syntax.AlternateUnsetOrNull: 697 | t.printf(`(test -n "$%s" && echo `, param) 698 | t.word(p.Exp.Word, true) 699 | t.str(" || echo)") 700 | case syntax.AlternateUnset: 701 | t.printf(`(set -q %s && echo `, param) 702 | t.word(p.Exp.Word, true) 703 | t.str(" || echo)") 704 | case syntax.DefaultUnsetOrNull: 705 | t.printf(`(test -n "$%s" && echo "$%s" || echo `, param, param) 706 | t.word(p.Exp.Word, true) 707 | t.str(")") 708 | case syntax.DefaultUnset: 709 | t.printf(`(set -q %s && echo "$%s" || echo `, param, param) 710 | t.word(p.Exp.Word, true) 711 | t.str(")") 712 | case syntax.RemSmallPrefix, syntax.RemLargePrefix, syntax.RemSmallSuffix, syntax.RemLargeSuffix: // a#a a##a a%a a%%a 713 | isPath := strings.HasSuffix(param, "PATH") 714 | suffix := op == syntax.RemSmallSuffix || op == syntax.RemLargeSuffix 715 | small := op == syntax.RemSmallPrefix || op == syntax.RemSmallSuffix 716 | var mode pattern.Mode 717 | if small { 718 | mode |= pattern.Shortest 719 | } 720 | pat, ok := lit(p.Exp.Word) 721 | if !ok { 722 | unsupported(p) 723 | } 724 | pat = unescape(pat) 725 | expr, err := pattern.Regexp(pat, mode) 726 | if err != nil { 727 | unsupported(p) 728 | } 729 | expr = strings.TrimPrefix(expr, "(?s)") 730 | dot := "" 731 | if isPath && (suffix && strings.HasSuffix(expr, ":") || !suffix && strings.HasPrefix(expr, ":")) { 732 | dot = `\.?` 733 | } 734 | if suffix { 735 | expr = "(" + expr + dot + ")$" 736 | } else { 737 | expr = "^(" + dot + expr + ")" 738 | } 739 | t.str(`(string replace -r `) 740 | t.escapedString(expr) 741 | t.printf(` '' "$%s")`, param) 742 | default: 743 | unsupported(p) 744 | } 745 | case p.Short: 746 | fallthrough 747 | default: 748 | if quoted { 749 | t.printf(`"$%s"`, param) 750 | } else { 751 | t.printf(`$%s`, param) 752 | } 753 | } 754 | } 755 | 756 | var stringReplacer = strings.NewReplacer("\\", "\\\\", "'", "\\'") 757 | 758 | func (t *Translator) capture(f func()) { 759 | oldBuf := t.buf 760 | newBuf := &bytes.Buffer{} 761 | t.buf = newBuf 762 | defer func() { 763 | t.buf = oldBuf 764 | t.escapedString(newBuf.String()) 765 | }() 766 | f() 767 | } 768 | 769 | func (t *Translator) escapedString(literal string) { 770 | t.str("'") 771 | stringReplacer.WriteString(t.buf, literal) 772 | t.str("'") 773 | } 774 | 775 | func (t *Translator) comment(c *syntax.Comment) { 776 | t.printf("#%s", c.Text) 777 | t.nl() 778 | } 779 | 780 | func (t *Translator) str(s string) { 781 | t.buf.WriteString(s) 782 | } 783 | 784 | func (t *Translator) printf(format string, arg ...interface{}) { 785 | fmt.Fprintf(t.buf, format, arg...) 786 | } 787 | 788 | func (t *Translator) indent() { 789 | t.indentLevel++ 790 | t.nl() 791 | } 792 | 793 | func (t *Translator) outdent() { 794 | t.indentLevel-- 795 | t.nl() 796 | } 797 | 798 | func (t *Translator) nl() { 799 | t.buf.WriteRune('\n') 800 | for i := 0; i < t.indentLevel; i++ { 801 | t.str(" ") 802 | } 803 | } 804 | 805 | func lit(w *syntax.Word) (string, bool) { 806 | // In the usual case, we'll have either a single part that's a literal, 807 | // or one of the parts being a non-literal. Using strings.Join instead 808 | // of a strings.Builder avoids extra work in these cases, since a single 809 | // part is a shortcut, and many parts don't incur string copies. 810 | lits := make([]string, 0, 1) 811 | for _, part := range w.Parts { 812 | lit, ok := part.(*syntax.Lit) 813 | if !ok { 814 | return "", false 815 | } 816 | lits = append(lits, lit.Value) 817 | } 818 | return strings.Join(lits, ""), true 819 | } 820 | 821 | func unescape(s string) string { 822 | if !strings.Contains(s, `\`) { 823 | return s 824 | } 825 | 826 | var buf bytes.Buffer 827 | for i := 0; i < len(s); i++ { 828 | b := s[i] 829 | // TODO: this is taken from sh, but it's wrong. The special characters depend on the context. 830 | // So I need a quote state variable in Translator 831 | if b == '\\' && i+1 < len(s) { 832 | switch s[i+1] { 833 | case '/', '"', '\\', '$', '`': // special chars 834 | continue 835 | } 836 | } 837 | buf.WriteByte(b) 838 | } 839 | 840 | return buf.String() 841 | } 842 | -------------------------------------------------------------------------------- /translate/translate_test.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/go-cmp/cmp" 6 | "mvdan.cc/sh/v3/syntax" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestEscapedString(t *testing.T) { 12 | tr := NewTranslator() 13 | tr.escapedString(`cool 'shit' yo`) 14 | s := tr.buf.String() 15 | equal(t, `'cool \'shit\' yo'`, s) 16 | } 17 | 18 | func equal(t testing.TB, wanted, actual interface{}) { 19 | if diff := cmp.Diff(wanted, actual); diff != "" { 20 | t.Errorf("%s", diff) 21 | fmt.Println(actual) 22 | } 23 | } 24 | 25 | func TestParse(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | in string 29 | expected string 30 | }{ 31 | { 32 | name: "chruby.sh", 33 | in: chruby, 34 | expected: chrubyExpected, 35 | }, 36 | { 37 | name: "test.sh", 38 | in: testFile, 39 | expected: testExpected, 40 | }, 41 | { 42 | name: "command-not-found.sh", 43 | in: nixIndexFile, 44 | expected: nixIndexExpected, 45 | }, 46 | { 47 | name: "java home", 48 | in: `if [ -z "${JAVA_HOME-}" ]; then export JAVA_HOME=/bla/lib/openjdk; fi`, 49 | expected: `if [ -z (set -q JAVA_HOME && echo "$JAVA_HOME" || echo '') ] 50 | set -gx JAVA_HOME '/bla/lib/openjdk' 51 | end 52 | `, 53 | }, 54 | { 55 | name: "recursive translation", 56 | in: `source /opt/source.sh`, 57 | expected: `/bin/babelfish < /opt/source.sh | source 58 | `, 59 | }, 60 | { 61 | name: "append to PATH", 62 | in: ` 63 | export NIX_PATH="nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix" 64 | export NIX_PATH="$HOME/.nix-defexpr/channels${NIX_PATH:+:$NIX_PATH}"`, 65 | expected: `set -gx NIX_PATH 'nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix' 66 | set -gx NIX_PATH "$HOME"'/.nix-defexpr/channels'(test -n "$NIX_PATH" && echo ':'"$NIX_PATH" || echo) 67 | `, 68 | }, 69 | { 70 | name: "unset function and variable", 71 | in: "unset -f foo -v bar", 72 | expected: `functions -e foo; set -e bar 73 | `, 74 | }, 75 | {name: "hash in name", 76 | in: `a=nixpkgs 77 | nix run $a#hello 78 | `, expected: `set a 'nixpkgs' 79 | nix run $a#hello 80 | `, 81 | }, 82 | } 83 | 84 | for _, test := range tests { 85 | t.Run(test.name, func(t *testing.T) { 86 | tr := NewTranslator() 87 | tr.babelFishLocation = "/bin/babelfish" 88 | p := syntax.NewParser(syntax.KeepComments(true), syntax.Variant(syntax.LangBash)) 89 | f, err := p.Parse(strings.NewReader(test.in), test.name) 90 | if err != nil { 91 | t.Error(err) 92 | return 93 | } 94 | err = tr.File(f) 95 | if err != nil { 96 | t.Error(err) 97 | return 98 | } 99 | s := tr.buf.String() 100 | equal(t, test.expected, s) 101 | }) 102 | } 103 | } 104 | 105 | const chruby = ` 106 | CHRUBY_VERSION="0.3.9" 107 | RUBIES=() 108 | 109 | for dir in "$PREFIX/opt/rubies" "$HOME/.rubies"; do 110 | [[ -d "$dir" && -n "$(ls -A "$dir")" ]] && RUBIES+=("$dir"/*) 111 | done 112 | unset dir 113 | 114 | function chruby_reset() 115 | { 116 | [[ -z "$RUBY_ROOT" ]] && return 117 | 118 | PATH=":$PATH:"; PATH="${PATH//:$RUBY_ROOT\/bin:/:}" 119 | [[ -n "$GEM_ROOT" ]] && PATH="${PATH//:$GEM_ROOT\/bin:/:}" 120 | 121 | if (( UID != 0 )); then 122 | [[ -n "$GEM_HOME" ]] && PATH="${PATH//:$GEM_HOME\/bin:/:}" 123 | 124 | GEM_PATH=":$GEM_PATH:" 125 | [[ -n "$GEM_HOME" ]] && GEM_PATH="${GEM_PATH//:$GEM_HOME:/:}" 126 | [[ -n "$GEM_ROOT" ]] && GEM_PATH="${GEM_PATH//:$GEM_ROOT:/:}" 127 | GEM_PATH="${GEM_PATH#:}"; GEM_PATH="${GEM_PATH%:}" 128 | 129 | unset GEM_HOME 130 | [[ -z "$GEM_PATH" ]] && unset GEM_PATH 131 | fi 132 | 133 | PATH="${PATH#:}"; PATH="${PATH%:}" 134 | unset RUBY_ROOT RUBY_ENGINE RUBY_VERSION RUBYOPT GEM_ROOT 135 | hash -r 136 | } 137 | 138 | function chruby_use() 139 | { 140 | if [[ ! -x "$1/bin/ruby" ]]; then 141 | echo "chruby: $1/bin/ruby not executable" >&2 142 | return 1 143 | fi 144 | 145 | [[ -n "$RUBY_ROOT" ]] && chruby_reset 146 | 147 | export RUBY_ROOT="$1" 148 | export RUBYOPT="$2" 149 | export PATH="$RUBY_ROOT/bin:$PATH" 150 | 151 | eval "$(RUBYGEMS_GEMDEPS="" "$RUBY_ROOT/bin/ruby" - <&2 202 | return 1 203 | fi 204 | 205 | shift 206 | chruby_use "$match" "$*" 207 | ;; 208 | esac 209 | } 210 | ` 211 | 212 | const chrubyExpected = `set CHRUBY_VERSION '0.3.9' 213 | set RUBIES 214 | for dir in "$PREFIX"'/opt/rubies' "$HOME"'/.rubies' 215 | test -d "$dir" && test -n (ls -A "$dir" | string collect; or echo) && set -a RUBIES "$dir"/* 216 | end 217 | set -e dir 218 | function chruby_reset 219 | test -z "$RUBY_ROOT" && return 220 | set PATH ':'"$PATH"':' 221 | set PATH (string replace --all ':'"$RUBY_ROOT"'/bin:' ':' "$PATH") 222 | test -n "$GEM_ROOT" && set PATH (string replace --all ':'"$GEM_ROOT"'/bin:' ':' "$PATH") 223 | if test (id -ru) -ne 0 224 | test -n "$GEM_HOME" && set PATH (string replace --all ':'"$GEM_HOME"'/bin:' ':' "$PATH") 225 | set GEM_PATH ':'"$GEM_PATH"':' 226 | test -n "$GEM_HOME" && set GEM_PATH (string replace --all ':'"$GEM_HOME"':' ':' "$GEM_PATH") 227 | test -n "$GEM_ROOT" && set GEM_PATH (string replace --all ':'"$GEM_ROOT"':' ':' "$GEM_PATH") 228 | set GEM_PATH (string replace -r '^(\\.?:)' '' "$GEM_PATH") 229 | set GEM_PATH (string replace -r '(:\\.?)$' '' "$GEM_PATH") 230 | set -e GEM_HOME 231 | test -z "$GEM_PATH" && set -e GEM_PATH 232 | end 233 | set PATH (string replace -r '^(\\.?:)' '' "$PATH") 234 | set PATH (string replace -r '(:\\.?)$' '' "$PATH") 235 | set -e RUBY_ROOT; set -e RUBY_ENGINE; set -e RUBY_VERSION; set -e RUBYOPT; set -e GEM_ROOT 236 | true 237 | end 238 | 239 | function chruby_use 240 | if test ! -x $argv[1]'/bin/ruby' 241 | echo 'chruby: '$argv[1]'/bin/ruby not executable' >&2 242 | return 1 243 | end 244 | test -n "$RUBY_ROOT" && chruby_reset 245 | set -gx RUBY_ROOT $argv[1] 246 | set -gx RUBYOPT $argv[2] 247 | set -gx PATH "$RUBY_ROOT"'/bin:'"$PATH" 248 | eval (RUBYGEMS_GEMDEPS='' "$RUBY_ROOT"'/bin/ruby' - <(echo 'puts "export RUBY_ENGINE=#{Object.const_defined?(:RUBY_ENGINE) ? RUBY_ENGINE : \'ruby\'};" 249 | puts "export RUBY_VERSION=#{RUBY_VERSION};" 250 | begin; require \'rubygems\'; puts "export GEM_ROOT=#{Gem.default_dir.inspect};"; rescue LoadError; end 251 | '| psub) | string collect; or echo) 252 | set -gx PATH (test -n "$GEM_ROOT" && echo "$GEM_ROOT"'/bin:' || echo)"$PATH" 253 | if test (id -ru) -ne 0 254 | set -gx GEM_HOME "$HOME"'/.gem/'"$RUBY_ENGINE"'/'"$RUBY_VERSION" 255 | set -gx GEM_PATH "$GEM_HOME"(test -n "$GEM_ROOT" && echo ':'"$GEM_ROOT" || echo)(test -n "$GEM_PATH" && echo ':'"$GEM_PATH" || echo) 256 | set -gx PATH "$GEM_HOME"'/bin:'"$PATH" 257 | end 258 | true 259 | end 260 | 261 | function chruby 262 | switch $argv[1] 263 | case '-h' '--help' 264 | echo 'usage: chruby [RUBY|VERSION|system] [RUBYOPT...]' 265 | case '-V' '--version' 266 | echo 'chruby: '"$CHRUBY_VERSION" 267 | case '' 268 | set -l dir $dir; set -l ruby $ruby 269 | for dir in $RUBIES 270 | set dir (string replace -r '(/)$' '' "$dir") 271 | set ruby (string replace -r '^(.*/)' '' "$dir") 272 | if test "$dir" = "$RUBY_ROOT" 273 | echo ' * '"$ruby"' '"$RUBYOPT" 274 | else 275 | echo ' '"$ruby" 276 | end 277 | end 278 | case 'system' 279 | chruby_reset 280 | case '*' 281 | set -l dir $dir; set -l ruby $ruby; set -l match $match 282 | for dir in $RUBIES 283 | set dir (string replace -r '(/)$' '' "$dir") 284 | set ruby (string replace -r '^(.*/)' '' "$dir") 285 | switch "$ruby" 286 | case $argv[1] 287 | set match "$dir" && break 288 | case '*'$argv[1]'*' 289 | set match "$dir" 290 | end 291 | end 292 | if test -z "$match" 293 | echo 'chruby: unknown Ruby: '$argv[1] >&2 294 | return 1 295 | end 296 | set -e argv[1] 297 | chruby_use "$match" "$argv" 298 | end 299 | end 300 | ` 301 | 302 | const testFile = ` 303 | #!/usr/bin/env bash 304 | 305 | # Prevent this file from being sourced by child shells. 306 | export __NIX_DARWIN_SET_ENVIRONMENT_DONE=1 307 | A=2 308 | C=3 echo 23 309 | export A 310 | 311 | export PATH=$HOME/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin 312 | export EDITOR="nano" 313 | export NIX_PATH="darwin-config=$HOME/dotfiles/darwin.nix:/nix/var/nix/profiles/per-user/root/channels:$HOME/.nix-defexpr/channels" 314 | export NIX_SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt" 315 | export PAGER="less -R" 316 | echo 123 | source 317 | cat <(echo 123) 318 | cat < test.bash 319 | cool() { 320 | cat | cat 321 | } 322 | echo $(cat test.bash | cool | (cool | cool | ( echo 'cool' | cool))) 323 | test -e /var/file.sh && source /var/file.sh 324 | if [ -z "$SSH_AUTH_SOCK" ]; then 325 | export SSH_AUTH_SOCK=$(/bin/gpgconf --list-dirs agent-ssh-socket) 326 | fi 327 | if [ -d "/share/gsettings-schemas/name" ]; then 328 | export whatevs=$whatevs${whatevs:+:}/share/gsettings-schemas/name 329 | elif false; then 330 | true 331 | else 332 | true 333 | fi 334 | echo ${cool+a} 335 | echo ${cool:+a} 336 | echo ${cool-a} 337 | echo ${cool:-a} 338 | unset ASPELL_CONF 339 | for i in a b c; do 340 | if [ -d "$i/lib/aspell" ]; then 341 | export ASPELL_CONF="dict-dir $i/lib/aspell" 342 | fi 343 | echo yes 344 | done 345 | for cmd 346 | do 347 | echo "$cmd" 348 | done 349 | time sleep 1 350 | while true; do 351 | echo 1 352 | echo 2 353 | done 354 | until true; do 355 | echo 1 356 | echo 2 357 | done 358 | call $me 359 | echo ${#@} 360 | echo ${#cool[@]} 361 | echo ${#cool} 362 | a=$(ok) 363 | a="$(ok)" 364 | . /etc/bashrc 365 | (( 123 )) 366 | ` 367 | 368 | const testExpected = `#!/usr/bin/env bash 369 | # Prevent this file from being sourced by child shells. 370 | set -gx __NIX_DARWIN_SET_ENVIRONMENT_DONE '1' 371 | set A '2' 372 | C='3' echo 23 373 | set -gx A $A 374 | set -gx PATH "$HOME"'/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin' 375 | set -gx EDITOR 'nano' 376 | set -gx NIX_PATH 'darwin-config='"$HOME"'/dotfiles/darwin.nix:/nix/var/nix/profiles/per-user/root/channels:'"$HOME"'/.nix-defexpr/channels' 377 | set -gx NIX_SSL_CERT_FILE '/etc/ssl/certs/ca-certificates.crt' 378 | set -gx PAGER 'less -R' 379 | echo 123 | source 380 | cat (echo 123 | psub) 381 | cat &2 echo "$1: command not found" 443 | return 127 444 | fi 445 | 446 | toplevel=nixpkgs # nixpkgs should always be available even in NixOS 447 | cmd=$1 448 | attrs=$(@out@/bin/nix-locate --minimal --no-group --type x --type s --top-level --whole-name --at-root "/bin/$cmd") 449 | len=$(echo -n "$attrs" | grep -c "^") 450 | 451 | case $len in 452 | 0) 453 | >&2 echo "$cmd: command not found" 454 | ;; 455 | 1) 456 | # if only 1 package provides this, then we can invoke it 457 | # without asking the users if they have opted in with one 458 | # of 2 environment variables 459 | 460 | # they are based on the ones found in 461 | # command-not-found.sh: 462 | 463 | # NIX_AUTO_INSTALL : install the missing command into the 464 | # user’s environment 465 | # NIX_AUTO_RUN : run the command transparently inside of 466 | # nix shell 467 | 468 | # these will not return 127 if they worked correctly 469 | 470 | if ! [ -z "${NIX_AUTO_INSTALL-}" ]; then 471 | >&2 cat <&2 cat <" 487 | if [ "$?" -eq 0 ]; then 488 | # how nix-shell handles commands is weird 489 | # $(echo $@) is need to handle this 490 | nix-shell -p $attrs --run "$(echo $@)" 491 | return $? 492 | else 493 | >&2 cat <&2 cat <&2 cat <&2 echo " nix-env -iA $toplevel.$attr" 516 | done <<< "$attrs" 517 | ;; 518 | esac 519 | 520 | return 127 # command not found should always exit with 127 521 | } 522 | 523 | # for zsh... 524 | # we just pass it to the bash handler above 525 | # apparently they work identically 526 | command_not_found_handler () { 527 | command_not_found_handle $@ 528 | return $? 529 | }` 530 | 531 | const nixIndexExpected = `#!/bin/sh 532 | # for bash 4 533 | # this will be called when a command is entered 534 | # but not found in the user’s path + environment 535 | function command_not_found_handle 536 | # TODO: use "command not found" gettext translations 537 | # taken from http://www.linuxjournal.com/content/bash-command-not-found 538 | # - do not run when inside Midnight Commander or within a Pipe 539 | if [ -n (set -q MC_SID && echo "$MC_SID" || echo '') ] || ! [ -t 1 ] 540 | echo $argv[1]': command not found' >&2 541 | return 127 542 | end 543 | # nixpkgs should always be available even in NixOS 544 | set toplevel 'nixpkgs' 545 | set cmd $argv[1] 546 | set attrs (@out@/bin/nix-locate --minimal --no-group --type x --type s --top-level --whole-name --at-root '/bin/'"$cmd" | string collect; or echo) 547 | set len (echo -n "$attrs" | grep -c '^' | string collect; or echo) 548 | switch "$len" 549 | case '0' 550 | echo "$cmd"': command not found' >&2 551 | case '1' 552 | # if only 1 package provides this, then we can invoke it 553 | # without asking the users if they have opted in with one 554 | # of 2 environment variables 555 | # they are based on the ones found in 556 | # command-not-found.sh: 557 | # NIX_AUTO_INSTALL : install the missing command into the 558 | # user’s environment 559 | # NIX_AUTO_RUN : run the command transparently inside of 560 | # nix shell 561 | # these will not return 127 if they worked correctly 562 | if ! [ -z (set -q NIX_AUTO_INSTALL && echo "$NIX_AUTO_INSTALL" || echo '') ] 563 | cat >&2 <(echo 'The program \''"$cmd"'\' is currently not installed. It is provided by 564 | the package \''"$toplevel"'.'"$attrs"'\', which I will now install for you. 565 | '| psub) 566 | nix-env -iA $toplevel.$attrs 567 | if [ "$status" -eq 0 ] 568 | # TODO: handle pipes correctly if AUTO_RUN/INSTALL is possible 569 | $argv 570 | return $status 571 | else 572 | cat >&2 <(echo 'Failed to install '"$toplevel"'.attrs. 573 | '"$cmd"': command not found 574 | '| psub) 575 | end 576 | else if ! [ -z (set -q NIX_AUTO_RUN && echo "$NIX_AUTO_RUN" || echo '') ] 577 | nix-build --no-out-link -A $attrs '<'"$toplevel"'>' 578 | if [ "$status" -eq 0 ] 579 | # how nix-shell handles commands is weird 580 | # $(echo $@) is need to handle this 581 | nix-shell -p $attrs --run (echo $argv | string collect; or echo) 582 | return $status 583 | else 584 | cat >&2 <(echo 'Failed to install '"$toplevel"'.attrs. 585 | '"$cmd"': command not found 586 | '| psub) 587 | end 588 | else 589 | cat >&2 <(echo 'The program \''"$cmd"'\' is currently not installed. You can install it 590 | by typing: 591 | nix-env -iA '"$toplevel"'.'"$attrs"' 592 | '| psub) 593 | end 594 | case '*' 595 | cat >&2 <(echo 'The program \''"$cmd"'\' is currently not installed. It is provided by 596 | several packages. You can install it by typing one of the following: 597 | '| psub) 598 | # ensure we get each element of attrs 599 | # in a cross platform way 600 | while read attr 601 | echo ' nix-env -iA '"$toplevel"'.'"$attr" >&2 602 | end <(echo "$attrs"| psub) 603 | end 604 | # command not found should always exit with 127 605 | return 127 606 | end 607 | 608 | # for zsh... 609 | # we just pass it to the bash handler above 610 | # apparently they work identically 611 | function command_not_found_handler 612 | command_not_found_handle $argv 613 | return $status 614 | end 615 | ` 616 | --------------------------------------------------------------------------------