├── main_test.go ├── .acceptance_tests ├── bats │ ├── fixtures │ │ ├── bash │ │ │ ├── helloworld.sh │ │ │ ├── unicode.sh │ │ │ ├── echochamber.sh │ │ │ └── shebang.sh │ │ ├── ruby │ │ │ ├── helloworld.rb │ │ │ ├── unicode.rb │ │ │ ├── shebang.rb │ │ │ └── echochamber.rb │ │ ├── fsharp │ │ │ ├── helloworld.fs │ │ │ ├── unicode.fs │ │ │ ├── shebang.fs │ │ │ └── echochamber.fs │ │ ├── perl │ │ │ ├── helloworld.pl │ │ │ ├── unicode.pl │ │ │ ├── shebang.pl │ │ │ └── echochamber.pl │ │ ├── python │ │ │ ├── helloworld.py │ │ │ ├── shebang.py │ │ │ ├── unicode.py │ │ │ └── echochamber.py │ │ ├── groovy │ │ │ ├── helloworld.groovy │ │ │ ├── unicode.groovy │ │ │ ├── shebang.groovy │ │ │ └── echochamber.groovy │ │ ├── lisp │ │ │ ├── helloworld.lisp │ │ │ ├── unicode.lisp │ │ │ ├── echochamber.lisp │ │ │ └── shebang.lisp │ │ ├── node │ │ │ ├── helloworld.js │ │ │ ├── unicode.js │ │ │ ├── shebang.js │ │ │ └── echochamber.js │ │ ├── scala │ │ │ ├── echochamber.scala │ │ │ ├── helloworld.scala │ │ │ ├── unicode.scala │ │ │ └── shebang.scala │ │ ├── clojure │ │ │ ├── helloworld.clj │ │ │ ├── unicode.clj │ │ │ ├── shebang.clj │ │ │ └── echochamber.clj │ │ ├── coffee │ │ │ ├── helloworld.coffee │ │ │ ├── unicode.coffee │ │ │ ├── echochamber.coffee │ │ │ └── shebang.coffee │ │ ├── haskell │ │ │ ├── helloworld.hs │ │ │ ├── unicode.hs │ │ │ ├── shebang.hs │ │ │ └── echochamber.hs │ │ ├── ocaml │ │ │ ├── helloworld.ml │ │ │ ├── unicode.ml │ │ │ ├── shebang.ml │ │ │ └── echochamber.ml │ │ ├── php │ │ │ ├── unicode.php │ │ │ ├── helloworld.php │ │ │ ├── shebang.php │ │ │ └── echochamber.php │ │ ├── racket │ │ │ ├── helloworld.rkt │ │ │ ├── unicode.rkt │ │ │ ├── shebang.rkt │ │ │ └── echochamber.rkt │ │ ├── rust │ │ │ ├── helloworld.rs │ │ │ ├── unicode.rs │ │ │ ├── shebang.rs │ │ │ └── echochamber.rs │ │ ├── d │ │ │ ├── helloworld.d │ │ │ ├── unicode.d │ │ │ ├── shebang.d │ │ │ └── echochamber.d │ │ ├── c │ │ │ ├── helloworld.c │ │ │ ├── unicode.c │ │ │ ├── shebang.c │ │ │ └── echochamber.c │ │ ├── go │ │ │ ├── helloworld.go │ │ │ ├── unicode.go │ │ │ ├── shebang.go │ │ │ └── echochamber.go │ │ ├── objc │ │ │ ├── helloworld.m │ │ │ ├── unicode.m │ │ │ ├── shebang.m │ │ │ └── echochamber.m │ │ ├── cpp │ │ │ ├── helloworld.cpp │ │ │ ├── unicode.cpp │ │ │ ├── shebang.cpp │ │ │ └── echochamber.cpp │ │ ├── erlang │ │ │ ├── helloworld.erl │ │ │ ├── unicode.erl │ │ │ ├── shebang.erl │ │ │ └── echochamber.erl │ │ ├── csharp │ │ │ ├── Unicode.cs │ │ │ ├── HelloWorld.cs │ │ │ ├── Shebang.cs │ │ │ └── EchoChamber.cs │ │ └── java │ │ │ ├── Unicode.java │ │ │ ├── HelloWorld.java │ │ │ ├── Shebang.java │ │ │ └── EchoChamber.java │ └── dexec.bats ├── Vagrantfile └── acceptance-tests.sh ├── Makefile ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .goxc.json ├── go.mod ├── LICENSE ├── util_test.go ├── CONTRIBUTING.md ├── util.go ├── dexec_test.go ├── CHANGELOG.md ├── main.go ├── dexec.go ├── README.md ├── cli.go ├── cli_test.go └── go.sum /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/bash/helloworld.sh: -------------------------------------------------------------------------------- 1 | echo "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ruby/helloworld.rb: -------------------------------------------------------------------------------- 1 | puts "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/bash/unicode.sh: -------------------------------------------------------------------------------- 1 | echo "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/fsharp/helloworld.fs: -------------------------------------------------------------------------------- 1 | printfn "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/perl/helloworld.pl: -------------------------------------------------------------------------------- 1 | print "hello world\n"; 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/python/helloworld.py: -------------------------------------------------------------------------------- 1 | print("hello world") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ruby/unicode.rb: -------------------------------------------------------------------------------- 1 | puts "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/fsharp/unicode.fs: -------------------------------------------------------------------------------- 1 | printfn "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/groovy/helloworld.groovy: -------------------------------------------------------------------------------- 1 | println "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/lisp/helloworld.lisp: -------------------------------------------------------------------------------- 1 | (format t "hello world~%") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/node/helloworld.js: -------------------------------------------------------------------------------- 1 | console.log("hello world") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/node/unicode.js: -------------------------------------------------------------------------------- 1 | console.log("hello unicode 👾") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/perl/unicode.pl: -------------------------------------------------------------------------------- 1 | print "hello unicode 👾\n"; 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/scala/echochamber.scala: -------------------------------------------------------------------------------- 1 | args.foreach(println) 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/scala/helloworld.scala: -------------------------------------------------------------------------------- 1 | println("hello world") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/scala/unicode.scala: -------------------------------------------------------------------------------- 1 | println("hello unicode 👾") 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : all test 2 | 3 | all: | test 4 | 5 | test: 6 | @go test 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/clojure/helloworld.clj: -------------------------------------------------------------------------------- 1 | (printf "%s\n" "hello world") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/coffee/helloworld.coffee: -------------------------------------------------------------------------------- 1 | console.log "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/coffee/unicode.coffee: -------------------------------------------------------------------------------- 1 | console.log "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/groovy/unicode.groovy: -------------------------------------------------------------------------------- 1 | println "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/haskell/helloworld.hs: -------------------------------------------------------------------------------- 1 | main = putStrLn "hello world" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/lisp/unicode.lisp: -------------------------------------------------------------------------------- 1 | (format t "hello unicode 👾~%") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ocaml/helloworld.ml: -------------------------------------------------------------------------------- 1 | print_string "hello world\n";; 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ocaml/unicode.ml: -------------------------------------------------------------------------------- 1 | print_string "hello unicode 👾\n";; 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/clojure/unicode.clj: -------------------------------------------------------------------------------- 1 | (printf "%s\n" "hello unicode 👾") 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/haskell/unicode.hs: -------------------------------------------------------------------------------- 1 | main = putStrLn "hello unicode 👾" 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/bash/echochamber.sh: -------------------------------------------------------------------------------- 1 | for arg in "${@}"; do echo "${arg}"; done 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/bash/shebang.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | echo "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ruby/shebang.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | puts "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/fsharp/shebang.fs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | printfn "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/node/shebang.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | console.log("hello world") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/perl/shebang.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | print "hello world\n"; 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/php/unicode.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/python/shebang.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | print("hello world") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ruby/echochamber.rb: -------------------------------------------------------------------------------- 1 | ARGV.each do |arg| 2 | puts "#{arg}" 3 | end 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/scala/shebang.scala: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | println("hello world") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/coffee/echochamber.coffee: -------------------------------------------------------------------------------- 1 | console.log arg for arg in process.argv[2..] 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/coffee/shebang.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | console.log "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/groovy/shebang.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | println "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/lisp/echochamber.lisp: -------------------------------------------------------------------------------- 1 | (loop for arg in *args* do (format t "~a~%" arg)) 2 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/lisp/shebang.lisp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | (format t "hello world~%") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ocaml/shebang.ml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | print_string "hello world\n";; 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/php/helloworld.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/python/unicode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | print("hello unicode 👾") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/racket/helloworld.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | (printf "~a~n" "hello world") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/racket/unicode.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | (printf "~a~n" "hello unicode 👾") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/rust/helloworld.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello world"); 3 | } 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/rust/unicode.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("hello unicode 👾"); 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .goxc.local.json 2 | .acceptance_tests/.vagrant 3 | .acceptance_tests/*.log 4 | .vscode/.* 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/clojure/shebang.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | (printf "%s\n" "hello world") 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/groovy/echochamber.groovy: -------------------------------------------------------------------------------- 1 | for ( arg in args ) { 2 | println arg 3 | } 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/haskell/shebang.hs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | main = putStrLn "hello world" 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/perl/echochamber.pl: -------------------------------------------------------------------------------- 1 | foreach my $arg (@ARGV){ 2 | print $arg, "\n"; 3 | } 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/python/echochamber.py: -------------------------------------------------------------------------------- 1 | import sys 2 | for arg in sys.argv[1:]: 3 | print(arg) 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/clojure/echochamber.clj: -------------------------------------------------------------------------------- 1 | (doseq [arg (drop 1 *command-line-args*)] 2 | (printf "%s\n" arg)) 3 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/php/shebang.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/racket/shebang.rkt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | #lang racket 3 | (printf "~a~n" "hello world") 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/rust/shebang.rs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | fn main() { 3 | println!("hello world"); 4 | } 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/d/helloworld.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | void main() 4 | { 5 | writeln("hello world"); 6 | } 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/d/unicode.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | void main() 4 | { 5 | writeln("hello unicode 👾"); 6 | } 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/node/echochamber.js: -------------------------------------------------------------------------------- 1 | for (var i = 2; i < process.argv.length; ++i) { 2 | console.log(process.argv[i]); 3 | } 4 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/d/shebang.d: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | import std.stdio; 3 | 4 | void main() 5 | { 6 | writeln("hello world"); 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/haskell/echochamber.hs: -------------------------------------------------------------------------------- 1 | import System.Environment 2 | 3 | main = do 4 | args <- getArgs 5 | mapM_ putStrLn args 6 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/c/helloworld.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("%s\n", "hello world"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/c/unicode.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("%s\n", "hello unicode 👾"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/fsharp/echochamber.fs: -------------------------------------------------------------------------------- 1 | [] 2 | let main args = 3 | args |> Array.iter (fun x -> printfn "%s" x) 4 | 0 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/go/helloworld.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Printf("%s\n", "hello world") 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/go/unicode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Printf("%s\n", "hello unicode 👾") 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/php/echochamber.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/objc/helloworld.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(void) 4 | { 5 | printf("%s\n", "hello world"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/objc/unicode.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(void) 4 | { 5 | printf("%s\n", "hello unicode 👾"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/cpp/helloworld.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | std::cout << "hello world" << std::endl; 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/erlang/helloworld.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | main([]) -> 3 | io:setopts([{encoding, unicode}]), 4 | io:fwrite("hello world\n"). 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/erlang/unicode.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | main([]) -> 3 | io:setopts([{encoding, unicode}]), 4 | io:fwrite("hello unicode 👾\n"). 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/c/shebang.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | #include 3 | 4 | int main() { 5 | printf("%s\n", "hello world"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/cpp/unicode.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | std::cout << "hello unicode 👾" << std::endl; 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/go/shebang.go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | package main 3 | 4 | import "fmt" 5 | 6 | func main() { 7 | fmt.Printf("%s\n", "hello world") 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/objc/shebang.m: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | #import 3 | 4 | int main(void) 5 | { 6 | printf("%s\n", "hello world"); 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/ocaml/echochamber.ml: -------------------------------------------------------------------------------- 1 | open Printf 2 | 3 | let () = 4 | for i = 1 to Array.length Sys.argv - 1 do 5 | printf "%s\n" Sys.argv.(i) 6 | done;; 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/csharp/Unicode.cs: -------------------------------------------------------------------------------- 1 | public class Unicode 2 | { 3 | public static void Main() 4 | { 5 | System.Console.WriteLine("hello unicode 👾"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/erlang/shebang.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | %% -*- erlang -*- 3 | main([]) -> 4 | io:setopts([{encoding, unicode}]), 5 | io:fwrite("hello world\n"). 6 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/java/Unicode.java: -------------------------------------------------------------------------------- 1 | public class Unicode { 2 | public static void main(String[] args) { 3 | System.out.println("hello unicode 👾"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/racket/echochamber.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | (for-each (lambda (arg) 3 | (printf "~a~n" arg)) 4 | (vector->list (current-command-line-arguments))) 5 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/cpp/shebang.cpp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | #include 3 | 4 | int main() 5 | { 6 | std::cout << "hello world" << std::endl; 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/csharp/HelloWorld.cs: -------------------------------------------------------------------------------- 1 | public class HelloWorld 2 | { 3 | public static void Main() 4 | { 5 | System.Console.WriteLine("hello world"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/java/HelloWorld.java: -------------------------------------------------------------------------------- 1 | public class HelloWorld { 2 | public static void main(String[] args) { 3 | System.out.println("hello world"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/d/echochamber.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | void main(string[] args) 4 | { 5 | foreach (string arg; args[1..$]) 6 | { 7 | writeln(arg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/csharp/Shebang.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | public class Shebang 3 | { 4 | public static void Main() 5 | { 6 | System.Console.WriteLine("hello world"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/java/Shebang.java: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dexec 2 | public class Shebang { 3 | public static void main(String[] args) { 4 | System.out.println("hello world"); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/rust/echochamber.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let args: Vec<_> = env::args().collect(); 5 | for arg in &args[1..] { 6 | println!("{}", arg); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/c/echochamber.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char ** argv) { 4 | for (int i = 1; i < argc; ++i) { 5 | printf("%s\n", argv[i]); 6 | } 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/go/echochamber.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | import "fmt" 5 | 6 | func main() { 7 | for _, arg := range os.Args[1:] { 8 | fmt.Printf("%s\n", arg) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/java/EchoChamber.java: -------------------------------------------------------------------------------- 1 | public class EchoChamber { 2 | public static void main(String[] args) { 3 | for (String arg : args) { 4 | System.out.println(arg); 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/objc/echochamber.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, char ** argv) 4 | { 5 | int i = 1; 6 | for (; i < argc; ++i) { 7 | printf("%s\n", argv[i]); 8 | } 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/cpp/echochamber.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char ** argv) 4 | { 5 | for (int i = 1; i < argc; ++i) 6 | { 7 | std::cout << argv[i] << std::endl; 8 | } 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/csharp/EchoChamber.cs: -------------------------------------------------------------------------------- 1 | public class HelloWorld 2 | { 3 | public static void Main(string[] args) 4 | { 5 | foreach (string arg in args) 6 | { 7 | System.Console.WriteLine(arg); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: thursday 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: github.com/fsouza/go-dockerclient 11 | versions: 12 | - 1.7.0 13 | - 1.7.1 14 | -------------------------------------------------------------------------------- /.goxc.json: -------------------------------------------------------------------------------- 1 | { 2 | "BuildConstraints": "windows,linux,darwin", 3 | "PackageVersion": "1.0.7", 4 | "TaskSettings": { 5 | "bintray": { 6 | "downloadspage": "bintray.md", 7 | "package": "dexec", 8 | "repository": "release", 9 | "subject": "dexec", 10 | "user": "docker-exec-bot" 11 | } 12 | }, 13 | "ConfigVersion": "0.9" 14 | } 15 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/fixtures/erlang/echochamber.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | 3 | format_list(L) -> 4 | fnl(L). 5 | 6 | fnl([H]) -> 7 | io:format("~s~n", [H]); 8 | 9 | fnl([H|T]) -> 10 | io:format("~s~n", [H]), 11 | fnl(T); 12 | 13 | fnl([]) -> 14 | ok. 15 | 16 | main(Args) -> 17 | io:setopts([{encoding, unicode}]), 18 | format_list(Args). 19 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [push] 3 | jobs: 4 | 5 | test: 6 | name: Build & Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.12 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.12 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Run Tests 20 | env: 21 | GO111MODULE: on 22 | run: go test -v 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker-exec/dexec 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 7 | github.com/docker/go-units v0.4.0 // indirect 8 | github.com/fsouza/go-dockerclient v1.6.4 9 | github.com/golang/protobuf v1.3.1 // indirect 10 | github.com/kisielk/errcheck v1.2.0 // indirect 11 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 12 | github.com/sirupsen/logrus v1.4.1 // indirect 13 | github.com/stretchr/objx v0.2.0 // indirect 14 | golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d 15 | golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect 16 | golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 // indirect 17 | golang.org/x/text v0.3.1 // indirect 18 | golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 Andy Stanton 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. -------------------------------------------------------------------------------- /.acceptance_tests/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = 'ubuntu/bionic64' 6 | 7 | config.vm.provider "virtualbox" do |v| 8 | v.name = "dexec-acceptance-tests" 9 | end 10 | 11 | config.vm.provision 'bootstrap', type: 'shell', inline: <<-SHELL 12 | function add_if_not_in_file() { 13 | if ! grep -q "$2" "$1"; then 14 | echo "$2" >> "$1" 15 | fi 16 | } 17 | 18 | sudo apt-get update 19 | sudo apt-get install -y curl git language-pack-en 20 | 21 | if [ ! -d /usr/local/go ]; then 22 | curl -sSL https://dl.google.com/go/go1.12.4.linux-amd64.tar.gz \ 23 | | tar -C /usr/local -xzf - 24 | fi 25 | 26 | if [ ! -x /usr/bin/docker ]; then 27 | curl -sSL https://get.docker.com/ | sh 28 | sudo usermod -aG docker vagrant 29 | fi 30 | 31 | if [ ! -x /usr/local/bin/bats ]; then 32 | bats_tmp=$(mktemp -d) 33 | git clone https://github.com/bats-core/bats-core.git $bats_tmp 34 | pushd $bats_tmp 2>/dev/null 35 | git checkout v1.1.0 36 | ./install.sh /usr/local 37 | popd 2>/dev/null 38 | rm -rf $bats_tmp 39 | fi 40 | 41 | add_if_not_in_file '/home/vagrant/.profile' 'export GOPATH=/home/vagrant/.go' 42 | add_if_not_in_file '/home/vagrant/.profile' 'export GO111MODULE=on' 43 | add_if_not_in_file '/home/vagrant/.profile' 'export PATH=$PATH:/usr/local/go/bin' 44 | add_if_not_in_file '/home/vagrant/.profile' 'export PATH=$PATH:$GOPATH/bin' 45 | 46 | touch /home/vagrant/.hushlogin 47 | mkdir -p /home/vagrant/.go/src/github.com/docker-exec/ 48 | chown -R vagrant:vagrant /home/vagrant/.go 49 | SHELL 50 | 51 | config.vm.synced_folder '..', '/home/vagrant/.go/src/github.com/docker-exec/dexec' 52 | end 53 | -------------------------------------------------------------------------------- /.acceptance_tests/acceptance-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function get_cwd() { 4 | pushd $(dirname ${0}) >/dev/null 5 | script_path=$(pwd -P) 6 | popd >/dev/null 7 | echo "${script_path}" 8 | } 9 | 10 | function get_snapshot_plugin() { 11 | if ! grep -q vagrant-vbox-snapshot <(vagrant plugin list); then 12 | vagrant plugin install vagrant-vbox-snapshot 13 | fi 14 | } 15 | 16 | function up() { 17 | vagrant status | awk 'NR==3' | grep -q 'not created' 18 | vm_does_not_exist=$? 19 | 20 | vagrant box list | grep -q ubuntu/bionic64 21 | has_box=$? 22 | 23 | if [[ ${has_box} -ne 0 ]]; then 24 | vagrant box add ubuntu/bionic64 25 | fi 26 | 27 | vagrant up 28 | 29 | if [[ ${vm_does_not_exist} -eq 0 ]]; then 30 | get_snapshot_plugin 31 | vagrant snapshot take 'post-bootstrap' 32 | fi 33 | } 34 | 35 | function down() { 36 | vagrant halt 37 | } 38 | 39 | function restore() { 40 | get_snapshot_plugin 41 | if grep 'post-bootstrap' <(vagrant snapshot list); then 42 | vagrant snapshot go 'post-bootstrap' 43 | fi 44 | } 45 | 46 | function run() { 47 | local cwd="$(get_cwd)" 48 | pushd "${cwd}" >/dev/null 49 | up 50 | if [[ -z "$1" ]] || [[ "$1" != "--no-clean" ]]; then 51 | echo "Restoring to initial state" 52 | restore 53 | else 54 | echo "Skipping restore to initial state" 55 | fi 56 | vagrant ssh -c " 57 | cd /home/vagrant/.go/src/github.com/docker-exec/dexec 58 | go get 59 | go install 60 | bats /bats/dexec.bats" 61 | down 62 | popd >/dev/null 63 | } 64 | 65 | function destroy() { 66 | local cwd="$(get_cwd)" 67 | pushd "${cwd}" >/dev/null 68 | echo "Destroying vagrant box" 69 | vagrant destroy -f 70 | 71 | popd >/dev/null 72 | } 73 | 74 | case $1 in 75 | run) 76 | run $2;; 77 | destroy) 78 | destroy;; 79 | *) 80 | echo "Usage:" 81 | echo " $0 run [--no-clean]" 82 | echo " $0 destroy" 83 | ;; 84 | esac 85 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSanitisePath(t *testing.T) { 9 | cases := []struct { 10 | path string 11 | platform string 12 | want string 13 | }{ 14 | {"/Users/foo/bar", "darin", "/Users/foo/bar"}, 15 | {"/home/foo/bar", "linux", "/home/foo/bar"}, 16 | {"C:\\Users\\foo\\bar", "windows", "/c/Users/foo/bar"}, 17 | } 18 | for _, c := range cases { 19 | gotSanitisedPath := SanitisePath(c.path, c.platform) 20 | if gotSanitisedPath != c.want { 21 | t.Errorf("SanitisedPath %q != %q", gotSanitisedPath, c.want) 22 | } 23 | } 24 | } 25 | 26 | func TestAddPrefix(t *testing.T) { 27 | cases := []struct { 28 | inSlice []string 29 | prefix string 30 | want []string 31 | }{ 32 | { 33 | []string{"foo", "bar", "foobar"}, 34 | "prefix", 35 | []string{"prefix", "foo", "prefix", "bar", "prefix", "foobar"}, 36 | }, 37 | } 38 | for _, c := range cases { 39 | got := AddPrefix(c.inSlice, c.prefix) 40 | if !reflect.DeepEqual(got, c.want) { 41 | t.Errorf("AddPrefix(%q, %q) %q != %q", c.inSlice, c.prefix, got, c.want) 42 | } 43 | } 44 | } 45 | 46 | func TestJoinStringSlices(t *testing.T) { 47 | cases := []struct { 48 | inSlices [][]string 49 | want []string 50 | }{ 51 | { 52 | [][]string{{"foo"}, {"bar"}, {"foobar"}}, 53 | []string{"foo", "bar", "foobar"}, 54 | }, 55 | } 56 | for _, c := range cases { 57 | got := JoinStringSlices(c.inSlices...) 58 | if !reflect.DeepEqual(got, c.want) { 59 | t.Errorf("JoinStringSlices(%q) %q != %q", c.inSlices, got, c.want) 60 | } 61 | } 62 | } 63 | 64 | func TestExtractFileExtension(t *testing.T) { 65 | cases := []struct { 66 | filename string 67 | extension string 68 | }{ 69 | {"foo.bar", "bar"}, 70 | {"foo.bar.foobar", "foobar"}, 71 | } 72 | for _, c := range cases { 73 | gotExtension := ExtractFileExtension(c.filename) 74 | if gotExtension != c.extension { 75 | t.Errorf("ExtractFileExtension %q != %q", gotExtension, c.extension) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.acceptance_tests/bats/dexec.bats: -------------------------------------------------------------------------------- 1 | @test "dexec is present" { 2 | run dexec -v 3 | [ "$status" -eq 0 ] 4 | grep -Ee '^dexec [[:digit:]]+.[[:digit:]]+.[[:digit:]]+(-SNAPSHOT)?$' <<<"$output" 5 | } 6 | 7 | function run_standard_tests() { 8 | pushd $BATS_TEST_DIRNAME/fixtures/$1 >/dev/null 9 | run dexec [Hh]ello[Ww]orld* 10 | [ "$status" -eq 0 ] 11 | [ "$output" = $'hello world\r' ] 12 | 13 | run dexec [Uu]nicode* 14 | [ "$status" -eq 0 ] 15 | [ "$output" = $'hello unicode 👾\r' ] 16 | 17 | run dexec [Ee]cho[Cc]hamber* -a hello -a world -a 'test with spaces' 18 | [ "$status" -eq 0 ] 19 | [ "${lines[0]}" = $'hello\r' ] 20 | [ "${lines[1]}" = $'world\r' ] 21 | [ "${lines[2]}" = $'test with spaces\r' ] 22 | 23 | run ./[Ss]hebang* 24 | [ "$status" -eq 0 ] 25 | [ "$output" = $'hello world\r' ] 26 | popd >/dev/null 27 | } 28 | 29 | @test "bash" { 30 | run_standard_tests $BATS_TEST_DESCRIPTION 31 | } 32 | 33 | @test "c" { 34 | run_standard_tests $BATS_TEST_DESCRIPTION 35 | } 36 | 37 | @test "clojure" { 38 | run_standard_tests $BATS_TEST_DESCRIPTION 39 | } 40 | 41 | @test "coffee" { 42 | run_standard_tests $BATS_TEST_DESCRIPTION 43 | } 44 | 45 | @test "cpp" { 46 | run_standard_tests $BATS_TEST_DESCRIPTION 47 | } 48 | 49 | @test "csharp" { 50 | run_standard_tests $BATS_TEST_DESCRIPTION 51 | } 52 | 53 | @test "d" { 54 | run_standard_tests $BATS_TEST_DESCRIPTION 55 | } 56 | 57 | @test "erlang" { 58 | run_standard_tests $BATS_TEST_DESCRIPTION 59 | } 60 | 61 | @test "fsharp" { 62 | run_standard_tests $BATS_TEST_DESCRIPTION 63 | } 64 | 65 | @test "go" { 66 | run_standard_tests $BATS_TEST_DESCRIPTION 67 | } 68 | 69 | @test "groovy" { 70 | run_standard_tests $BATS_TEST_DESCRIPTION 71 | } 72 | 73 | @test "haskell" { 74 | run_standard_tests $BATS_TEST_DESCRIPTION 75 | } 76 | 77 | @test "java" { 78 | run_standard_tests $BATS_TEST_DESCRIPTION 79 | } 80 | 81 | @test "lisp" { 82 | run_standard_tests $BATS_TEST_DESCRIPTION 83 | } 84 | 85 | @test "node" { 86 | run_standard_tests $BATS_TEST_DESCRIPTION 87 | } 88 | 89 | @test "objc" { 90 | run_standard_tests $BATS_TEST_DESCRIPTION 91 | } 92 | 93 | @test "ocaml" { 94 | run_standard_tests $BATS_TEST_DESCRIPTION 95 | } 96 | 97 | @test "perl" { 98 | run_standard_tests $BATS_TEST_DESCRIPTION 99 | } 100 | 101 | @test "php" { 102 | run_standard_tests $BATS_TEST_DESCRIPTION 103 | } 104 | 105 | @test "python" { 106 | run_standard_tests $BATS_TEST_DESCRIPTION 107 | } 108 | 109 | @test "racket" { 110 | run_standard_tests $BATS_TEST_DESCRIPTION 111 | } 112 | 113 | @test "ruby" { 114 | run_standard_tests $BATS_TEST_DESCRIPTION 115 | } 116 | 117 | @test "rust" { 118 | run_standard_tests $BATS_TEST_DESCRIPTION 119 | } 120 | 121 | @test "scala" { 122 | run_standard_tests $BATS_TEST_DESCRIPTION 123 | } 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | These are the guidelines for contributing to the ```dexec``` CLI and also applies to the associated Docker images and tooling projects. 4 | 5 | ## Creating issues 6 | 7 | Please consult the [reference section](https://github.com/docker-exec/dexec/blob/master/README.md#reference) of the README before creating issues. It may contain the answer to your problem. 8 | 9 | ## Code 10 | 11 | Fork the repo. Make your changes. Raise a PR. 12 | 13 | All PRs are welcome, and changes don't just have to be additive - if you have ideas about how to improve existing code, please feel free to submit these. 14 | 15 | ```dexec``` is written in go and changes must adhere to the default settings for all the following tools: 16 | 17 | * go vet 18 | * gofmt 19 | * golint 20 | 21 | A good place to start is with the [go-plus](https://github.com/joefitzgerald/go-plus/) package for [Atom](https://atom.io/) which automatically verifies the code against those tools. 22 | 23 | ## Dependencies 24 | 25 | Dependencies are managed using the experimental vendor feature introduced in Go 1.5. Versioned library code is committed to the dexec repo to guarantee reproducible builds, and the tool used to achieve this is [Godep](https://github.com/tools/godep). Please read the [Godep documentation](https://github.com/tools/godep#add-a-dependency) if you need to add a new library. 26 | 27 | ## Unit Tests 28 | 29 | Unit tests are required for new contributions in most cases. Don't break the existing ones. Run the following command from the path that you checked out dexec to run the unit tests: 30 | 31 | ```sh 32 | go test ./... 33 | ``` 34 | 35 | ## Acceptance Tests 36 | 37 | A suite of acceptance tests can be found in the [dexec/_bats](https://github.com/docker-exec/dexec/tree/master/_test) folder. If you are adding a new image, you must also add to the acceptance tests. 38 | 39 | Vagrant's VirtualBox provider is used to spin up an Ubuntu VM on which Docker is installed. The current ```dexec``` folder is mounted in the VM, built and then run against four [bats](https://github.com/sstephenson/bats) tests for each docker-exec image. The tests are: 40 | 41 | * Verify simple output. 42 | * Verify unicode output. 43 | * Verify execution arguments are available to script. 44 | * Verify code can be run as standalone script. 45 | 46 | Run the following command from the path that you checked out dexec to run the acceptance tests: 47 | 48 | ```sh 49 | acceptance_tests/acceptance_tests.sh run 50 | ``` 51 | 52 | By default, the vagrant box will be restored to its initial state at the start of every run. This guarantees sandboxing of the application and images, but means that all the images will be redownloaded every time you run the tests. This may not be desirable during development as it takes several minutes to download the images. To avoid this you can add the ```--no-clean``` option to reuse images that have already been downloaded to the VM: 53 | 54 | ```sh 55 | acceptance_tests/acceptance_tests.sh run --no-clean 56 | ``` 57 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | const sanitisedWindowsPathPattern = "/%s%s" 15 | 16 | // SanitisePath takes an absolute path as provided by filepath.Abs() and 17 | // makes it ready to be passed to Docker based on the current OS. So far 18 | // the only OS format that requires transforming is Windows which is provided 19 | // in the form 'C:\some\path' but Docker requires '/c/some/path'. 20 | func SanitisePath(path string, platform string) string { 21 | sanitised := path 22 | if platform == "windows" { 23 | windowsPathPattern := regexp.MustCompile("^([A-Za-z]):(.*)") 24 | match := windowsPathPattern.FindStringSubmatch(path) 25 | 26 | driveLetter := strings.ToLower(match[1]) 27 | pathRemainder := strings.Replace(match[2], "\\", "/", -1) 28 | 29 | sanitised = fmt.Sprintf(sanitisedWindowsPathPattern, driveLetter, pathRemainder) 30 | } 31 | return sanitised 32 | } 33 | 34 | // RetrievePath takes an array whose first element may contain an overridden 35 | // path and converts either this, or the default of "." to an absolute path 36 | // using Go's file utilities. This is then passed to SanitisedPath with the 37 | // current OS to get it into a Docker ready format. 38 | func RetrievePath(targetDirs []string) string { 39 | path := "." 40 | if len(targetDirs) > 0 { 41 | path = targetDirs[0] 42 | } 43 | absPath, _ := filepath.Abs(path) 44 | return SanitisePath(absPath, runtime.GOOS) 45 | } 46 | 47 | // AddPrefix takes a string slice and returns a new string slice 48 | // with the supplied prefix inserted before every string in the 49 | // original slice. 50 | func AddPrefix(inSlice []string, prefix string) []string { 51 | var outSlice []string 52 | for _, option := range inSlice { 53 | outSlice = append(outSlice, []string{prefix, option}...) 54 | } 55 | return outSlice 56 | } 57 | 58 | // JoinStringSlices takes an arbitrary number of string slices 59 | // and concatenates them in the order supplied. 60 | func JoinStringSlices(slices ...[]string) []string { 61 | var outSlice []string 62 | for _, slice := range slices { 63 | outSlice = append(outSlice, slice...) 64 | } 65 | return outSlice 66 | } 67 | 68 | // ExtractFileExtension extracts the extension from a filename. This is defined 69 | // as the remainder of the string after the last '.'. 70 | func ExtractFileExtension(filename string) string { 71 | patternPermission := regexp.MustCompile(`.*\.(.*):.*`) 72 | permissionMatch := patternPermission.FindStringSubmatch(filename) 73 | if len(permissionMatch) > 0 { 74 | return permissionMatch[1] 75 | } 76 | patternFilename := regexp.MustCompile(`.*\.(.*)`) 77 | return patternFilename.FindStringSubmatch(filename)[1] 78 | } 79 | 80 | // WriteFile writes a file. 81 | func WriteFile(filename string, content []byte) { 82 | if err := ioutil.WriteFile(filename, content, 0644); err != nil { 83 | log.Fatalf("Unable to write %s\n%q", filename, err) 84 | } 85 | } 86 | 87 | // DeleteFile deletes a file. 88 | func DeleteFile(filename string) { 89 | if err := os.Remove(filename); err != nil { 90 | log.Fatalf("Unable to delete %s\n%q", filename, err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dexec_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBuildVolumeArgs(t *testing.T) { 9 | cases := []struct { 10 | path string 11 | targets []string 12 | wantVolumes []string 13 | }{ 14 | {"/foo", []string{"bar"}, []string{"/foo/bar:/tmp/dexec/build/bar"}}, 15 | } 16 | for _, c := range cases { 17 | gotVolumes := BuildVolumeArgs(c.path, c.targets) 18 | if !reflect.DeepEqual(gotVolumes, c.wantVolumes) { 19 | t.Errorf("BuildVolumeArgs(%q, %q) %q != %q", c.path, c.targets, gotVolumes, c.wantVolumes) 20 | } 21 | } 22 | } 23 | 24 | func TestLookupImageByOverride(t *testing.T) { 25 | cases := []struct { 26 | image string 27 | extension string 28 | wantExtension string 29 | wantImage string 30 | wantVersion string 31 | wantError error 32 | }{ 33 | {"dexec/cpp", "c", "c", "dexec/cpp", "latest", nil}, 34 | {"dexec/some-language", "ext", "ext", "dexec/some-language", "latest", nil}, 35 | {"dexec/some-language:1.2.3", "ext", "ext", "dexec/some-language", "1.2.3", nil}, 36 | } 37 | for _, c := range cases { 38 | got, err := LookupImageByOverride(c.image, c.extension) 39 | if got.Image != c.wantImage { 40 | t.Errorf("LookupImageByOverride(%q, %q) %q != %q", c.image, c.extension, got.Image, c.wantImage) 41 | } else if got.Extension != c.wantExtension { 42 | t.Errorf("LookupImageByOverride(%q, %q) %q != %q", c.image, c.extension, got.Extension, c.wantExtension) 43 | } else if got.Version != c.wantVersion { 44 | t.Errorf("LookupImageByOverride(%q, %q) %q != %q", c.image, c.extension, got.Version, c.wantVersion) 45 | } else if err != c.wantError { 46 | t.Errorf("LookupImageByOverride(%q, %q) %q != %q", c.image, c.extension, err, c.wantError) 47 | } 48 | } 49 | } 50 | 51 | func TestLookupImageByExtension(t *testing.T) { 52 | cases := []struct { 53 | extension string 54 | wantExtension string 55 | wantImage string 56 | wantVersion string 57 | wantError error 58 | }{ 59 | {"c", "c", "dexec/lang-c", "1.0.2", nil}, 60 | {"clj", "clj", "dexec/lang-clojure", "1.0.1", nil}, 61 | {"coffee", "coffee", "dexec/lang-coffee", "1.0.2", nil}, 62 | {"cpp", "cpp", "dexec/lang-cpp", "1.0.2", nil}, 63 | {"cs", "cs", "dexec/lang-csharp", "1.0.2", nil}, 64 | {"d", "d", "dexec/lang-d", "1.0.1", nil}, 65 | {"erl", "erl", "dexec/lang-erlang", "1.0.1", nil}, 66 | {"fs", "fs", "dexec/lang-fsharp", "1.0.2", nil}, 67 | {"go", "go", "dexec/lang-go", "1.0.1", nil}, 68 | {"groovy", "groovy", "dexec/lang-groovy", "1.0.1", nil}, 69 | {"hs", "hs", "dexec/lang-haskell", "1.0.1", nil}, 70 | {"java", "java", "dexec/lang-java", "1.0.3", nil}, 71 | {"lisp", "lisp", "dexec/lang-lisp", "1.0.1", nil}, 72 | {"lua", "lua", "dexec/lang-lua", "1.0.1", nil}, 73 | {"js", "js", "dexec/lang-node", "1.0.2", nil}, 74 | {"m", "m", "dexec/lang-objc", "1.0.2", nil}, 75 | {"ml", "ml", "dexec/lang-ocaml", "1.0.1", nil}, 76 | {"nim", "nim", "dexec/lang-nim", "1.0.1", nil}, 77 | {"p6", "p6", "dexec/lang-perl6", "1.0.1", nil}, 78 | {"pl", "pl", "dexec/lang-perl", "1.0.2", nil}, 79 | {"php", "php", "dexec/lang-php", "1.0.1", nil}, 80 | {"py", "py", "dexec/lang-python", "1.0.2", nil}, 81 | {"r", "r", "dexec/lang-r", "1.0.1", nil}, 82 | {"rkt", "rkt", "dexec/lang-racket", "1.0.1", nil}, 83 | {"rb", "rb", "dexec/lang-ruby", "1.0.2", nil}, 84 | {"rs", "rs", "dexec/lang-rust", "1.0.1", nil}, 85 | {"scala", "scala", "dexec/lang-scala", "1.0.1", nil}, 86 | {"sh", "sh", "dexec/lang-bash", "1.0.1", nil}, 87 | } 88 | for _, c := range cases { 89 | got, err := LookupImageByExtension(c.extension) 90 | if got.Image != c.wantImage { 91 | t.Errorf("TestLookupExtensionByImage(%q) %q != %q", c.extension, got.Image, c.wantImage) 92 | } else if got.Extension != c.wantExtension { 93 | t.Errorf("TestLookupExtensionByImage(%q) %q != %q", c.extension, got.Extension, c.wantExtension) 94 | } else if got.Version != c.wantVersion { 95 | t.Errorf("TestLookupExtensionByImage(%q) %q != %q", c.extension, got.Version, c.wantVersion) 96 | } else if err != c.wantError { 97 | t.Errorf("TestLookupExtensionByImage(%q) %q != %q", c.extension, err, c.wantError) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased][unreleased] 6 | ### Added 7 | - Destroy option in acceptance test script. 8 | - Github Actions support. 9 | 10 | ### Fixed 11 | - Fixed Stdin example in Readme.md. 12 | 13 | ### Changed 14 | - Migrate to Go Modules for dependency management. 15 | - Move sources to root. 16 | - Renamed Image struct to ContainerImage to avoid clash with Image enum value. 17 | - Removed .vscode dir. 18 | - Renamed .test dir to acceptance_tests 19 | - Updated years in LICENSE. 20 | 21 | ### Removed 22 | - Removed redundant stdin reading code. 23 | - Removed Travis support. 24 | 25 | ## [1.0.7] - 2016-09-18 26 | ### Fixed 27 | - Compatibility with Docker remote API 1.24. 28 | 29 | ## [1.0.6] - 2016-04-24 30 | ### Fixed 31 | - Shebang support for Java files now works. 32 | - Ruby and Objective C no longer output 'stdin: not a tty' before program output. 33 | - Regex for extracting source filenames no longer ignores single character filenames e.g. 'a.cpp'. 34 | 35 | ### Changed 36 | - Re-enabled Java, Ruby and Objective C in acceptance tests. 37 | 38 | ## [1.0.5] - 2016-04-23 39 | ### Fixed 40 | - Receive input from STDIN correctly and display STDOUT/STDERR messages only once. 41 | 42 | ## [1.0.4] - 2016-04-23 43 | ### Added 44 | - Added ability to read from STDIN either manually or via pipe. 45 | - Added ability to specify image to use by file extension. 46 | - Added clean functionality to remove all locally downloaded dexec images. 47 | - Add Vagrant-based acceptance tests. 48 | - Added installation instructions for Homebrew on OSX. 49 | - Add contributing instructions to readme. 50 | - Add vscode tasks configuration. 51 | 52 | ### Changed 53 | - Migrated from custom code that called the Docker CLI to the library 'fsouza/go-dockerclient' which uses the Docker Remote API. 54 | - Deleted custom code that called the Docker CLI. 55 | - Switch to go vendoring for dependency management using Godeps. 56 | 57 | ## [1.0.3] - 2015-11-13 58 | ### Fixed 59 | - Forward application output to stdout/stderr correctly. 60 | 61 | ## [1.0.2] - 2015-05-12 62 | ### Changed 63 | - Moved Docker, CLI & miscellaneous functionality to separate packages. 64 | - Bumped patch versions of each image by 1 to enable unicode support introduced by those versions. 65 | - Changed the format of the image names from ```dexec/{{language abbreviation}}``` to ```dexec/lang-{{language abbreviation}}```. 66 | - Add contributors section in the readme. 67 | 68 | ## [1.0.1] - 2015-04-20 69 | ### Added 70 | - New languages: R, Nim and Lua. 71 | - Support for Perl 6 via .p6 extension. 72 | - Ability to specify the image used with --specify-image or -s. 73 | 74 | ### Changed 75 | - Perl 5 is now the default for .pl extension. 76 | - Version command is now suffixed with newline. 77 | - Fixed typo in RunDexecContainer comments. 78 | 79 | ### Fixed 80 | - Bug in IsDockerPresent and IsDockerPresent where defer method was not correctly called on panic. 81 | - Corrected how paths are handled in Windows allowing volumes to be mounted. 82 | 83 | ## 1.0.0 - 2015-04-06 84 | ### Added 85 | - Command line interface 'dexec'. 86 | - Ability to pass source files to container. 87 | - Container image selected based on source file extension. 88 | - Support for Bash, C, Clojure, CoffeeScript, C++, C#, D, Erlang, F#, Go, Groovy, Haskell, Java, Lisp, Node JS, Objective C, OCaml, Perl, PHP, Python, Racket, Ruby, Rust & Scala. 89 | - Ability to pass arguments to the language's compiler if it has one with --build-arg or -b. 90 | - Ability to pass arguments to the executing code with --arg or -a. 91 | - Ability to pass other files or folders to be mounted in the container with --include or -i. 92 | - Ability to augment source files with a shebang resulting in dexec being called. 93 | - Help dialog. 94 | - Version dialog. 95 | 96 | [unreleased]: https://github.com/docker-exec/dexec/compare/v1.0.3...HEAD 97 | [1.0.3]: https://github.com/docker-exec/dexec/compare/v1.0.2...v1.0.3 98 | [1.0.2]: https://github.com/docker-exec/dexec/compare/v1.0.1...v1.0.2 99 | [1.0.1]: https://github.com/docker-exec/dexec/compare/v1.0.0...v1.0.1 100 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/crypto/ssh/terminal" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "time" 11 | 12 | docker "github.com/fsouza/go-dockerclient" 13 | ) 14 | 15 | const timeoutStatusCode = 124 16 | 17 | // RunDexecContainer runs an anonymous Docker container with a Docker Exec 18 | // image, mounting the specified sources and includes and passing the 19 | // list of sources and arguments to the entrypoint. 20 | func RunDexecContainer(cliParser CLI) int { 21 | options := cliParser.Options 22 | 23 | shouldClean := len(options[CleanFlag]) > 0 24 | updateImage := len(options[UpdateFlag]) > 0 25 | 26 | client, err := docker.NewClientFromEnv() 27 | 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | if shouldClean { 33 | images, err := client.ListImages(docker.ListImagesOptions{ 34 | All: true, 35 | }) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | for _, image := range images { 40 | for _, tag := range image.RepoTags { 41 | repoRegex := regexp.MustCompile("^dexec/lang-[^:\\s]+(:.+)?$") 42 | if match := repoRegex.MatchString(tag); match { 43 | if err := client.RemoveImage(image.ID); err != nil { 44 | log.Fatalf("cannot remove image %s", image.ID) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | dexecImage, err := ImageFromOptions(options) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | dockerImage := fmt.Sprintf("%s:%s", dexecImage.Image, dexecImage.Version) 57 | 58 | if err = FetchImage( 59 | dexecImage.Image, 60 | dexecImage.Version, 61 | updateImage, 62 | client); err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | var sourceBasenames []string 67 | for _, source := range options[Source] { 68 | basename, _ := ExtractBasenameAndPermission(source) 69 | sourceBasenames = append(sourceBasenames, []string{basename}...) 70 | } 71 | 72 | entrypointArgs := JoinStringSlices( 73 | sourceBasenames, 74 | AddPrefix(options[BuildArg], "-b"), 75 | AddPrefix(options[Arg], "-a"), 76 | ) 77 | 78 | readFromStdin := false 79 | 80 | if stat, _ := os.Stdin.Stat(); (stat.Mode() & os.ModeCharDevice) == 0 { 81 | readFromStdin = true 82 | } else { 83 | fd := int(os.Stdin.Fd()) 84 | if terminal.IsTerminal(fd) { 85 | oldState, err := terminal.MakeRaw(fd) 86 | if err != nil { 87 | log.Fatalf("could not make terminal raw: %s", err) 88 | } 89 | defer func() { 90 | if err := terminal.Restore(fd, oldState); err != nil { 91 | log.Fatalf("couldn't restore terminal: %s", err) 92 | } 93 | }() 94 | } 95 | } 96 | 97 | container, err := client.CreateContainer(docker.CreateContainerOptions{ 98 | Config: &docker.Config{ 99 | Image: dockerImage, 100 | Cmd: entrypointArgs, 101 | StdinOnce: true, 102 | OpenStdin: true, 103 | AttachStdin: true, 104 | AttachStderr: true, 105 | AttachStdout: true, 106 | Tty: !readFromStdin, 107 | }, 108 | HostConfig: &docker.HostConfig{ 109 | Binds: BuildVolumeArgs( 110 | RetrievePath(options[TargetDir]), 111 | append(options[Source], options[Include]...)), 112 | }, 113 | }) 114 | 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | 119 | defer func() { 120 | if err = client.RemoveContainer(docker.RemoveContainerOptions{ 121 | ID: container.ID, 122 | }); err != nil { 123 | log.Fatal(err) 124 | } 125 | }() 126 | 127 | success := make(chan struct{}) 128 | waiter, err := client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{ 129 | Container: container.ID, 130 | InputStream: os.Stdin, 131 | OutputStream: os.Stdout, 132 | ErrorStream: os.Stderr, 133 | Stream: true, 134 | Stdin: true, 135 | Stdout: true, 136 | Stderr: true, 137 | Logs: false, 138 | RawTerminal: !readFromStdin, 139 | Success: success, 140 | }) 141 | if err != nil { 142 | log.Fatalf("unable to send attach to container request: %s", err) 143 | } 144 | <-success 145 | close(success) 146 | 147 | if err = client.StartContainer(container.ID, &docker.HostConfig{}); err != nil { 148 | log.Fatalf("unable to start container: %s", err) 149 | } 150 | 151 | if timeout, ok := options[Timeout]; ok && len(timeout) == 1 { 152 | type ClientRunResult struct { 153 | Code int 154 | Error error 155 | } 156 | 157 | done := make(chan ClientRunResult) 158 | go func() { 159 | if err := waiter.Wait(); err != nil { 160 | log.Fatalf("unable to attach to container: %s", err) 161 | } 162 | 163 | code, err := client.WaitContainer(container.ID) 164 | result := ClientRunResult{ 165 | code, 166 | err, 167 | } 168 | done <- result 169 | }() 170 | 171 | num, _ := strconv.Atoi(timeout[0]) 172 | t := time.Duration(num) * time.Second 173 | timeout := time.After(t) 174 | 175 | select { 176 | case <-timeout: 177 | err := client.KillContainer(docker.KillContainerOptions{ID: container.ID}) 178 | if err != nil { 179 | log.Fatal(err) 180 | } 181 | return timeoutStatusCode 182 | case result := <-done: 183 | code := result.Code 184 | err := result.Error 185 | if err != nil { 186 | log.Fatal(err) 187 | } 188 | return code 189 | } 190 | } else { 191 | if err := waiter.Wait(); err != nil { 192 | log.Fatalf("unable to attach to container: %s", err) 193 | } 194 | 195 | code, err := client.WaitContainer(container.ID) 196 | if err != nil { 197 | log.Fatal(err) 198 | } 199 | return code 200 | } 201 | } 202 | 203 | func validate(cliParser CLI) bool { 204 | options := cliParser.Options 205 | 206 | hasVersionFlag := len(options[VersionFlag]) == 1 207 | hasSources := len(options[Source]) > 0 208 | shouldClean := len(options[CleanFlag]) > 0 209 | 210 | if hasSources || shouldClean { 211 | return true 212 | } 213 | 214 | if hasVersionFlag { 215 | DisplayVersion(cliParser.Filename) 216 | return false 217 | } 218 | 219 | DisplayHelp(cliParser.Filename) 220 | return false 221 | } 222 | 223 | func validateDocker() error { 224 | client, err := docker.NewClientFromEnv() 225 | if err != nil { 226 | return err 227 | } 228 | 229 | ping := make(chan error, 1) 230 | go func() { 231 | ping <- client.Ping() 232 | }() 233 | 234 | select { 235 | case err := <-ping: 236 | return err 237 | case <-time.After(5 * time.Second): 238 | return fmt.Errorf("request to Docker host timed out") 239 | } 240 | } 241 | 242 | func main() { 243 | cliParser := ParseOsArgs(os.Args) 244 | 245 | if validate(cliParser) { 246 | if err := validateDocker(); err != nil { 247 | log.Fatal(err) 248 | } else { 249 | os.Exit(RunDexecContainer(cliParser)) 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /dexec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | 8 | "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | // ContainerImage consists of the file extension, Docker image name and Docker 12 | // image version to use for a given Docker Exec image. 13 | type ContainerImage struct { 14 | Name string 15 | Extension string 16 | Image string 17 | Version string 18 | } 19 | 20 | const dexecPath = "/tmp/dexec/build" 21 | const dexecImageTemplate = "%s:%s" 22 | const dexecVolumeTemplate = "%s/%s:%s/%s" 23 | 24 | // ImageFromOptions returns an image from a set of options. 25 | func ImageFromOptions(options map[OptionType][]string) (image *ContainerImage, err error) { 26 | useExtension := len(options[Extension]) == 1 27 | useImage := len(options[Image]) == 1 28 | 29 | if useStdin := len(options[Source]) == 0; useStdin { 30 | if useExtension { 31 | image, err = LookupImageByExtension(options[Extension][0]) 32 | } else if useImage { 33 | overrideImage, err := LookupImageByOverride(options[Image][0], "unknown") 34 | if err != nil { 35 | return nil, err 36 | } 37 | image, err = LookupImageByName(overrideImage.Image) 38 | if image != nil { 39 | image.Version = overrideImage.Version 40 | } 41 | } else { 42 | err = fmt.Errorf("STDIN requested but no extension or image supplied") 43 | } 44 | } else { 45 | if extension := ExtractFileExtension(options[Source][0]); useExtension { 46 | image, err = LookupImageByExtension(options[Extension][0]) 47 | } else if useImage { 48 | image, err = LookupImageByOverride(options[Image][0], extension) 49 | } else { 50 | image, err = LookupImageByExtension(extension) 51 | } 52 | } 53 | return image, err 54 | } 55 | 56 | // BuildVolumeArgs takes a base path and returns an array of Docker volume 57 | // arguments. The array takes the form {"-v", "/foo:/bar:[rw|ro]", ...} for 58 | // each source or include. 59 | func BuildVolumeArgs(path string, targets []string) []string { 60 | var volumeArgs []string 61 | 62 | for _, source := range targets { 63 | basename, _ := ExtractBasenameAndPermission(source) 64 | 65 | volumeArgs = append( 66 | volumeArgs, 67 | fmt.Sprintf(dexecVolumeTemplate, path, basename, dexecPath, source), 68 | ) 69 | } 70 | return volumeArgs 71 | } 72 | 73 | // ExtractBasenameAndPermission takes an include string and splits it into 74 | // its file or folder name and the permission string if present or the empty 75 | // string if not. 76 | func ExtractBasenameAndPermission(path string) (string, string) { 77 | pathPattern := regexp.MustCompile("([\\w.:-]+)(:(rw|ro))") 78 | match := pathPattern.FindStringSubmatch(path) 79 | 80 | basename := path 81 | var permission string 82 | 83 | if len(match) == 4 { 84 | basename = match[1] 85 | permission = match[2] 86 | } 87 | return basename, permission 88 | } 89 | 90 | // FetchImage guarantees a Docker image is availabe in the local repository or 91 | // returns an error. 92 | func FetchImage(name string, tag string, update bool, client *docker.Client) error { 93 | dockerImage := fmt.Sprintf(dexecImageTemplate, name, tag) 94 | 95 | if _, err := client.InspectImage(dockerImage); update || err != nil { 96 | err = client.PullImage(docker.PullImageOptions{ 97 | Repository: name, 98 | Tag: tag, 99 | }, docker.AuthConfiguration{}) 100 | 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | 105 | if _, err = client.InspectImage(dockerImage); err != nil { 106 | return err 107 | } 108 | } 109 | return nil 110 | } 111 | 112 | var innerMap = map[string]*ContainerImage{ 113 | "c": {"C", "c", "dexec/lang-c", "1.0.2"}, 114 | "clj": {"Clojure", "clj", "dexec/lang-clojure", "1.0.1"}, 115 | "coffee": {"CoffeeScript", "coffee", "dexec/lang-coffee", "1.0.2"}, 116 | "cpp": {"C++", "cpp", "dexec/lang-cpp", "1.0.2"}, 117 | "cs": {"C#", "cs", "dexec/lang-csharp", "1.0.2"}, 118 | "d": {"D", "d", "dexec/lang-d", "1.0.1"}, 119 | "erl": {"Erlang", "erl", "dexec/lang-erlang", "1.0.1"}, 120 | "fs": {"F#", "fs", "dexec/lang-fsharp", "1.0.2"}, 121 | "go": {"Go", "go", "dexec/lang-go", "1.0.1"}, 122 | "groovy": {"Groovy", "groovy", "dexec/lang-groovy", "1.0.1"}, 123 | "hs": {"Haskell", "hs", "dexec/lang-haskell", "1.0.1"}, 124 | "java": {"Java", "java", "dexec/lang-java", "1.0.3"}, 125 | "lisp": {"Lisp", "lisp", "dexec/lang-lisp", "1.0.1"}, 126 | "lua": {"Lua", "lua", "dexec/lang-lua", "1.0.1"}, 127 | "js": {"JavaScript", "js", "dexec/lang-node", "1.0.2"}, 128 | "nim": {"Nim", "nim", "dexec/lang-nim", "1.0.1"}, 129 | "m": {"Objective C", "m", "dexec/lang-objc", "1.0.2"}, 130 | "ml": {"OCaml", "ml", "dexec/lang-ocaml", "1.0.1"}, 131 | "p6": {"Perl 6", "p6", "dexec/lang-perl6", "1.0.1"}, 132 | "pl": {"Perl", "pl", "dexec/lang-perl", "1.0.2"}, 133 | "php": {"PHP", "php", "dexec/lang-php", "1.0.1"}, 134 | "py": {"Python", "py", "dexec/lang-python", "1.0.2"}, 135 | "r": {"R", "r", "dexec/lang-r", "1.0.1"}, 136 | "rkt": {"Racket", "rkt", "dexec/lang-racket", "1.0.1"}, 137 | "rb": {"Ruby", "rb", "dexec/lang-ruby", "1.0.2"}, 138 | "rs": {"Rust", "rs", "dexec/lang-rust", "1.0.1"}, 139 | "scala": {"Scala", "scala", "dexec/lang-scala", "1.0.1"}, 140 | "sh": {"Bash", "sh", "dexec/lang-bash", "1.0.1"}, 141 | } 142 | 143 | // LookupImageByExtension returns the image for a given extension. 144 | func LookupImageByExtension(key string) (*ContainerImage, error) { 145 | if v, ok := innerMap[key]; ok { 146 | return v, nil 147 | } 148 | return nil, fmt.Errorf("map does not contain key %s", key) 149 | } 150 | 151 | // LookupImageByName returns the image for a given image name. 152 | func LookupImageByName(name string) (*ContainerImage, error) { 153 | for _, v := range innerMap { 154 | if v.Image == name { 155 | return v, nil 156 | } 157 | } 158 | return nil, fmt.Errorf("map does not contain image with name %s", name) 159 | } 160 | 161 | // LookupImageByOverride takes an image that has been specified by the user 162 | // to use instead of the one in the extension map. This function returns a 163 | // DexecImage struct containing the image name & version, as well as the 164 | // file extension that was passed in. 165 | func LookupImageByOverride(image string, extension string) (*ContainerImage, error) { 166 | patternImage := regexp.MustCompile(`(.*):(.*)`) 167 | imageMatch := patternImage.FindStringSubmatch(image) 168 | if len(imageMatch) > 0 { 169 | return &ContainerImage{ 170 | "Unknown", 171 | extension, 172 | imageMatch[1], 173 | imageMatch[2], 174 | }, nil 175 | } 176 | return &ContainerImage{ 177 | "Unknown", 178 | extension, 179 | image, 180 | "latest", 181 | }, nil 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dexec ![Build Status](https://github.com/docker-exec/dexec/workflows/Unit%20Tests/badge.svg) [ ![Download](https://api.bintray.com/packages/dexec/release/dexec/images/download.svg) ](https://bintray.com/dexec/release/dexec/_latestVersion) 2 | 3 | A command line utility for executing code in many different languages using the Docker Exec images, written in Go. 4 | 5 | ![dexec demo animation](https://docker-exec.github.io/images/dexec-short-1.0.1.gif) 6 | 7 | ## Installation 8 | 9 | ### Using Bintray 10 | 11 | Download the appropriate binary for your OS and architecture, then unzip or untar and move the ```dexec``` executable to where it can be found on your PATH. 12 | 13 | | OS | 64-bit | 32-bit | 14 | | ------- | ------ | ------ | 15 | | Linux | [64-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_linux_amd64.tar.gz) | [32-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_linux_386.tar.gz) | 16 | | Mac | [64-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_darwin_amd64.zip) | [32-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_darwin_386.zip) | 17 | | Windows | [64-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_windows_amd64.zip) | [32-bit](https://bintray.com/artifact/download/dexec/release/dexec_1.0.7_windows_386.zip) | 18 | 19 | Binaries for other distributions are available on [Bintray](https://bintray.com/dexec/release/dexec/_latestVersion). 20 | 21 | ### Using Go 22 | 23 | Install with the ```go get``` command. 24 | 25 | ```sh 26 | $ go get github.com/docker-exec/dexec 27 | ``` 28 | 29 | ### Using Homebrew 30 | 31 | If you're on OSX you can install the latest release of ```dexec``` with brew. 32 | 33 | ```sh 34 | $ brew install docker-exec/formula/dexec 35 | ``` 36 | 37 | ## Reference 38 | 39 | These examples use a .cpp source file, but any of the supported languages can be used instead. Arguments can be passed in any order, using any style of the acceptable switch styles described. 40 | 41 | The application provides help and version information as follows: 42 | 43 | ```sh 44 | $ dexec --version 45 | $ dexec --help 46 | ``` 47 | 48 | ### Pass source files to execute 49 | 50 | Multiple source files can be passed to the compiler or interpreter as follows. The first source file's extension is used to pick the appropriate Docker Exec image, e.g. .cpp retrieves dexec/cpp from the Docker registry. 51 | 52 | ```sh 53 | $ dexec foo.cpp 54 | $ dexec foo.cpp bar.cpp 55 | ``` 56 | 57 | The sources are mounted individually using the default Docker mount permissions (rw) and can be specified by appending :ro or :rw to the source file. 58 | 59 | ### Pass arguments for build 60 | 61 | For compiled languages, arguments can be passed to the compiler. 62 | 63 | ```sh 64 | $ dexec foo.cpp --build-arg=-std=c++11 65 | $ dexec foo.cpp --build-arg -std=c++11 66 | $ dexec foo.cpp -b -std=c++11 67 | ``` 68 | 69 | ### Pass arguments for execution 70 | 71 | Arguments can be passed to the executing code. Enclose arguments with single quotes to preserve whitespace. 72 | 73 | ```sh 74 | $ dexec foo.cpp --arg=hello --arg=world --arg='hello world' 75 | $ dexec foo.cpp --arg hello --arg world --arg 'hello world' 76 | $ dexec foo.cpp -a hello -a world -a 'hello world' 77 | ``` 78 | 79 | ### Specify location of source files 80 | 81 | By default, ```dexec``` assumes the sources are in the directory from which it is being invoked from. It is possible to override the working directory by passing the ```-C``` flag. 82 | 83 | ```sh 84 | $ dexec -C /path/to/sources foo.cpp bar.cpp 85 | ``` 86 | 87 | ### Read from STDIN 88 | 89 | ```dexec``` will forward your terminal's STDIN to the executing code. You can redirect from a file or use pipe: 90 | 91 | ```sh 92 | $ dexec foo.cpp 171 | int main() { 172 | std::cout << "hello world" << std::endl; 173 | } 174 | ``` 175 | 176 | then 177 | 178 | ```sh 179 | $ chmod +x foo.cpp 180 | $ ./foo.cpp 181 | ``` 182 | 183 | ## Contributors 184 | 185 | #### [docker-exec/dexec](https://github.com/docker-exec/dexec/graphs/contributors) 186 | 187 | * [John H. Ayad](https://github.com/johnhany97) 188 | * [Alix Axel](https://github.com/alixaxel) 189 | * [kroton](https://github.com/kroton) 190 | * [John Albietz](https://github.com/inthecloud247) 191 | 192 | #### [docker-exec/perl](https://github.com/docker-exec/perl/graphs/contributors) 193 | 194 | * [Øyvind Skaar](https://github.com/oyvindsk) 195 | 196 | ## See also 197 | 198 | * [Docker Exec GitHub Page](https://docker-exec.github.io/) 199 | * [Docker Exec GitHub Repositories](https://github.com/docker-exec) 200 | * [Docker Exec Images on Docker Hub](https://hub.docker.com/r/dexec/) 201 | * [dexec on Bintray](https://bintray.com/dexec/release/dexec/view) 202 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // OptionType allows for the enumeration of different CLI option types. 9 | type OptionType int 10 | 11 | const ( 12 | // None indicates that no option type is available. 13 | None OptionType = iota 14 | 15 | // Arg indicates that the option is an argument to be passed to the 16 | // executing code. 17 | Arg OptionType = iota 18 | 19 | // BuildArg indicates that the option is an argument to be passed to the 20 | // compiler if one is used for the target language. 21 | BuildArg OptionType = iota 22 | 23 | // Source indicates that the option is a source file. 24 | Source OptionType = iota 25 | 26 | // Include indicates that the option is a file or folder to be mounted 27 | // in the Docker container without passing it to the compiler or 28 | // executing code. 29 | Include OptionType = iota 30 | 31 | // Image indicates that the option value should be used to 32 | // override the image worked out based on the file extension. 33 | Image OptionType = iota 34 | 35 | // TargetDir indicates that the option specifies a custom location (i.e. 36 | // not the current working directory) to which the sources are 37 | // relatively specified. 38 | TargetDir OptionType = iota 39 | 40 | // UpdateFlag indicates that the option specifies that Docker images should 41 | // be manually updated before being used. 42 | UpdateFlag OptionType = iota 43 | 44 | // HelpFlag indicates that the option specifies the help flag. 45 | HelpFlag OptionType = iota 46 | 47 | // VersionFlag indicates that the option specifies the version flag. 48 | VersionFlag OptionType = iota 49 | 50 | // Extension specifies the override file extenision to use. 51 | Extension OptionType = iota 52 | 53 | // CleanFlag indicates that the option specifies the clean flag. 54 | CleanFlag OptionType = iota 55 | 56 | // Timeout indicates that the option specifies the timeout flag. 57 | Timeout OptionType = iota 58 | ) 59 | 60 | // CLI defines a data structure that represents the application's name and 61 | // a map of the various options to be used when starting the container. 62 | type CLI struct { 63 | Filename string 64 | Options map[OptionType][]string 65 | } 66 | 67 | // ArgToOption takes two candidate strings and returns a tuple consisting of 68 | // what type of option the strings define, the value of the option, how many 69 | // of the strings it took to extract the option value, and a nillable error. 70 | func ArgToOption(opt string, next string) (OptionType, string, int, error) { 71 | patternStandaloneA := regexp.MustCompile(`^-(a|-arg)$`) 72 | patternStandaloneB := regexp.MustCompile(`^-(b|-build-arg)$`) 73 | patternStandaloneI := regexp.MustCompile(`^-(i|-include)$`) 74 | patternStandaloneM := regexp.MustCompile(`^-(m|-image)$`) 75 | patternStandaloneE := regexp.MustCompile(`^-(e|-extension)$`) 76 | patternStandaloneT := regexp.MustCompile(`^-(t|-timeout)$`) 77 | patternStandaloneC := regexp.MustCompile(`^-C$`) 78 | patternCombinationA := regexp.MustCompile(`^--arg=(.+)$`) 79 | patternCombinationB := regexp.MustCompile(`^--build-arg=(.+)$`) 80 | patternCombinationI := regexp.MustCompile(`^--include=(.+)$`) 81 | patternCombinationM := regexp.MustCompile(`^--image=(.+)$`) 82 | patternCombinationE := regexp.MustCompile(`^--extension=(.+)$`) 83 | patternCombinationT := regexp.MustCompile(`^--timeout=(.+)$`) 84 | patternSource := regexp.MustCompile(`^[^-_].*\..+`) 85 | patternUpdateFlag := regexp.MustCompile(`^-(-update|u)$`) 86 | patternHelpFlag := regexp.MustCompile(`^-(-help|h)$`) 87 | patternVersionFlag := regexp.MustCompile(`^-(-version|v)$`) 88 | patternCleanFlag := regexp.MustCompile(`^--clean$`) 89 | 90 | switch { 91 | case patternStandaloneA.FindStringIndex(opt) != nil: 92 | return Arg, next, 2, nil 93 | case patternStandaloneB.FindStringIndex(opt) != nil: 94 | return BuildArg, next, 2, nil 95 | case patternStandaloneI.FindStringIndex(opt) != nil: 96 | return Include, next, 2, nil 97 | case patternStandaloneM.FindStringIndex(opt) != nil: 98 | return Image, next, 2, nil 99 | case patternStandaloneE.FindStringIndex(opt) != nil: 100 | return Extension, next, 2, nil 101 | case patternStandaloneC.FindStringIndex(opt) != nil: 102 | return TargetDir, next, 2, nil 103 | case patternStandaloneT.FindStringIndex(opt) != nil: 104 | return Timeout, next, 2, nil 105 | case patternCombinationA.FindStringIndex(opt) != nil: 106 | return Arg, patternCombinationA.FindStringSubmatch(opt)[1], 1, nil 107 | case patternCombinationB.FindStringIndex(opt) != nil: 108 | return BuildArg, patternCombinationB.FindStringSubmatch(opt)[1], 1, nil 109 | case patternCombinationI.FindStringIndex(opt) != nil: 110 | return Include, patternCombinationI.FindStringSubmatch(opt)[1], 1, nil 111 | case patternCombinationM.FindStringIndex(opt) != nil: 112 | return Image, patternCombinationM.FindStringSubmatch(opt)[1], 1, nil 113 | case patternCombinationE.FindStringIndex(opt) != nil: 114 | return Extension, patternCombinationE.FindStringSubmatch(opt)[1], 1, nil 115 | case patternCombinationT.FindStringIndex(opt) != nil: 116 | return Timeout, patternCombinationT.FindStringSubmatch(opt)[1], 1, nil 117 | case patternUpdateFlag.FindStringIndex(opt) != nil: 118 | return UpdateFlag, "", 1, nil 119 | case patternHelpFlag.FindStringIndex(opt) != nil: 120 | return HelpFlag, "", 1, nil 121 | case patternVersionFlag.FindStringIndex(opt) != nil: 122 | return VersionFlag, "", 1, nil 123 | case patternCleanFlag.FindStringIndex(opt) != nil: 124 | return CleanFlag, "", 1, nil 125 | case patternSource.FindStringIndex(opt) != nil: 126 | return Source, opt, 1, nil 127 | default: 128 | return None, "", 0, fmt.Errorf("unknown option: %s", opt) 129 | } 130 | } 131 | 132 | // ParseArgs take a string slice comprised of sources, includes, flags, switches 133 | // and their values and returns a map of these types to a string slice 134 | // of the values of each type of option. 135 | func ParseArgs(args []string) map[OptionType][]string { 136 | if len(args) == 0 { 137 | return map[OptionType][]string{} 138 | } 139 | 140 | var next string 141 | if len(args) > 1 { 142 | next = args[1] 143 | } 144 | 145 | optionType, optionValue, nextIndex, _ := ArgToOption(args[0], next) 146 | 147 | if len(args) < nextIndex || nextIndex == 0 { 148 | return map[OptionType][]string{} 149 | } 150 | 151 | optionMap := ParseArgs(args[nextIndex:]) 152 | optionMap[optionType] = append([]string{optionValue}, optionMap[optionType]...) 153 | return optionMap 154 | } 155 | 156 | // ParseOsArgs takes a string slice representing the full arguments passed to 157 | // the program, including the filename and returns a CLI containing the 158 | // filename and map of option types to their values. 159 | func ParseOsArgs(args []string) CLI { 160 | return CLI{ 161 | Filename: args[0], 162 | Options: ParseArgs(args[1:]), 163 | } 164 | } 165 | 166 | // DisplayHelp takes a filename and prints the help information for the program. 167 | func DisplayHelp(filename string) { 168 | fmt.Println("Name:") 169 | fmt.Printf("\t%s - Execute code in many languages with Docker!\n", filename) 170 | fmt.Println() 171 | fmt.Println("Usage:") 172 | fmt.Printf("\t%s [options] \n", filename) 173 | fmt.Println() 174 | fmt.Println("Options:") 175 | fmt.Printf("\t%-36s%s\n", "-C ", "Specify source directory") 176 | fmt.Printf("\t%-36s%s\n", "--arg, -a ", "Pass to the executing code") 177 | fmt.Printf("\t%-36s%s\n", "--build-arg, -b ", "Pass to compiler") 178 | fmt.Printf("\t%-36s%s\n", "--include, -i ", "Mount local in dexec container") 179 | fmt.Printf("\t%-36s%s\n", "--extension, -e ", "Override the image used by ") 180 | fmt.Printf("\t%-36s%s\n", "--timeout, -t