├── .gitignore ├── data ├── templates │ ├── chicken.scm │ ├── v.v │ ├── dart_multifile_project │ │ ├── functions.dart │ │ └── main.dart │ ├── haskell.hs │ ├── pascal.pp │ ├── fortran.f90 │ ├── zig.zig │ ├── racket.rkt │ ├── dart.dart │ ├── ocaml.ml │ ├── reasonml.re │ ├── cobol.cob │ ├── deno.ts │ ├── c.c │ ├── crystal.cr │ ├── cpp.cc │ ├── dart_multifile.dart │ ├── sbcl.lisp │ ├── go.go │ ├── docker │ ├── elixir.ex │ ├── docker-c.c │ ├── node.js │ ├── python-mypy.py │ ├── docker-ocaml.ml │ ├── docker-reason.re │ ├── docker-rust.rs │ ├── rust.rs │ ├── csharp.cs │ ├── python-pip.py │ └── java.java └── README.md ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ ├── on-push.yml │ └── on-release.yml ├── AUTHORS ├── man ├── scriptisto-template-ls.1 ├── scriptisto-cache-info.1 ├── scriptisto-template-import.1 ├── scriptisto-template-rm.1 ├── scriptisto-cache-get.1 ├── scriptisto-template-edit.1 ├── scriptisto-new.1 ├── scriptisto-cache-clean.1 ├── scriptisto-build.1 ├── scriptisto-cache.1 ├── scriptisto-template.1 └── scriptisto.1 ├── src ├── editor.rs ├── common.rs ├── cfg.rs ├── cache.rs ├── opt.rs ├── main.rs ├── templates.rs └── build.rs ├── README.md ├── Cargo.toml ├── CODE_OF_CONDUCT.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.swp 3 | -------------------------------------------------------------------------------- /data/templates/chicken.scm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | ; scriptisto-begin 4 | ; script_src: script.scm 5 | ; build_cmd: csc -O2 script.scm 6 | ; scriptisto-end 7 | 8 | (print "Hello, Scheme!") 9 | -------------------------------------------------------------------------------- /data/templates/v.v: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | // scriptisto-begin 3 | // script_src: script.v 4 | // build_cmd: v -prod script.v && strip script 5 | // scriptisto-end 6 | 7 | println('Hello, World!') 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Tests pass 6 | - [ ] Appropriate changes to README are included in PR -------------------------------------------------------------------------------- /data/templates/dart_multifile_project/functions.dart: -------------------------------------------------------------------------------- 1 | // This file will also be checked for changes by scriptisto 2 | 3 | import 'dart:io'; 4 | 5 | String getWorkingDirectory() { 6 | return Directory.current.path; 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: -------------------------------------------------------------------------------- /data/templates/dart_multifile_project/main.dart: -------------------------------------------------------------------------------- 1 | import 'functions.dart'; 2 | 3 | void main(List arguments) { 4 | print( 5 | 'Hello Dart, arguments=$arguments getWorkingDirectory()=${getWorkingDirectory()}'); 6 | } 7 | -------------------------------------------------------------------------------- /data/templates/haskell.hs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | -- scriptisto-begin 4 | -- script_src: script.hs 5 | -- build_cmd: ghc -O -o script script.hs && strip ./script 6 | -- scriptisto-end 7 | 8 | main = putStrLn "Hello, Haskell!" 9 | -------------------------------------------------------------------------------- /data/templates/pascal.pp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: script.pp 5 | // build_cmd: fpc -O2 script.pp 6 | // scriptisto-end 7 | 8 | program script; 9 | 10 | begin 11 | writeln('Hello, Pascal!'); 12 | end. 13 | -------------------------------------------------------------------------------- /data/templates/fortran.f90: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | ! scriptisto-begin 4 | ! script_src: script.f90 5 | ! build_cmd: gfortran script.f90 -o ./script 6 | ! scriptisto-end 7 | 8 | program script 9 | implicit none 10 | print *, "Hello, Fortran!" 11 | end program script 12 | -------------------------------------------------------------------------------- /data/templates/zig.zig: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | // scriptisto-begin 3 | // script_src: script.zig 4 | // build_cmd: zig build-exe script.zig 5 | // scriptisto-end 6 | 7 | const std = @import("std"); 8 | 9 | pub fn main() anyerror!void { 10 | std.log.info("Hello, World!", .{}); 11 | } 12 | -------------------------------------------------------------------------------- /data/templates/racket.rkt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | ; scriptisto-begin 4 | ; script_src: ./script.rkt 5 | ; build_cmd: raco make script.rkt 6 | ; target_bin: ./compiled/script_rkt.zo 7 | ; target_interpreter: /usr/bin/env racket -t 8 | ; scriptisto-end 9 | 10 | #lang racket/base 11 | (displayln "Hello, Racket!") 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Scriptisto authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | Google LLC 7 | Igor Petruk 8 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | Data from this directory is linked in inside of the binary. 4 | 5 | ## Script templates 6 | 7 | For convinience of distributing a single binary that requires no installation, files in `templates` directory are linked into the binary. They are stripped offile extensions and shown as templates via `scriptisto new` command. 8 | -------------------------------------------------------------------------------- /data/templates/dart.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | /* 3 | scriptisto-begin 4 | script_src: cli/main.dart 5 | build_cmd: dart compile exe "cli/main.dart" -o my-dart-program 6 | target_bin: ./my-dart-program 7 | scriptisto-end 8 | */ 9 | 10 | void main(List arguments) { 11 | print('Hello Dart, arguments=' + arguments.toString()); 12 | } 13 | -------------------------------------------------------------------------------- /data/templates/ocaml.ml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | (* 4 | scriptisto-begin 5 | script_src: script.ml 6 | build_cmd: dune build script.exe 7 | target_bin: ./_build/default/script.exe 8 | files: 9 | - path: dune 10 | content: (executable (name script) (libraries lwt.unix)) 11 | scriptisto-end 12 | *) 13 | 14 | Lwt_main.run (Lwt_io.printf "Hello, OCaml!\n") 15 | -------------------------------------------------------------------------------- /data/templates/reasonml.re: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | /* 4 | scriptisto-begin 5 | script_src: script.re 6 | build_cmd: dune build script.exe 7 | target_bin: ./_build/default/script.exe 8 | files: 9 | - path: dune 10 | content: (executable (name script) (libraries lwt.unix)) 11 | scriptisto-end 12 | */ 13 | 14 | Lwt_main.run (Lwt_io.printf ("Hello, ReasonML!\n")); 15 | -------------------------------------------------------------------------------- /man/scriptisto-template-ls.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-template-ls 1 "scriptisto-template-ls " 4 | .SH NAME 5 | scriptisto\-template\-ls \- List all templates 6 | .SH SYNOPSIS 7 | \fBscriptisto template ls\fR [\fB\-h\fR|\fB\-\-help\fR] 8 | .SH DESCRIPTION 9 | List all templates 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | -------------------------------------------------------------------------------- /data/templates/cobol.cob: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | * scriptisto-begin 3 | * script_src: cobol.cob 4 | * build_cmd: cobc -x -o script ./cobol.cob 5 | * replace_shebang_with: ' * ' 6 | * scriptisto-end 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. hello. 9 | PROCEDURE DIVISION. 10 | DISPLAY "Hello, COBOL!". 11 | STOP RUN. 12 | 13 | -------------------------------------------------------------------------------- /data/templates/deno.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: script.ts 5 | // build_cmd: deno bundle script.ts 6 | // target_bin: ./script.bundle.js 7 | // target_interpreter: /usr/bin/env deno --no-prompt 8 | // scriptisto-end 9 | 10 | import { yellow, bold } from "https://deno.land/std/fmt/colors.ts"; 11 | 12 | console.log(yellow(bold("Hello, TypeScript on Deno!"))); 13 | -------------------------------------------------------------------------------- /data/templates/c.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | #include 4 | #include 5 | 6 | // scriptisto-begin 7 | // script_src: main.c 8 | // build_cmd: clang -O2 main.c `pkg-config --libs --cflags glib-2.0` -o ./script 9 | // scriptisto-end 10 | 11 | int main(int argc, char* argv[]) { 12 | const gchar* user = g_getenv("USER"); 13 | printf("Hello, C! Current user: %s\n", user); 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /data/templates/crystal.cr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | # scriptisto-begin 4 | # script_src: script.cr 5 | # build_cmd: shards build --production && strip ./bin/script 6 | # target_bin: ./bin/script 7 | # files: 8 | # - path: shard.yml 9 | # content: | 10 | # name: script 11 | # version: 0.1.0 12 | # targets: 13 | # script: 14 | # main: script.cr 15 | # scriptisto-end 16 | 17 | puts "Hello, Crystal!" 18 | -------------------------------------------------------------------------------- /data/templates/cpp.cc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | #include 4 | #include 5 | 6 | // scriptisto-begin 7 | // script_src: main.cc 8 | // build_cmd: clang++ -O2 main.cc `pkg-config --libs --cflags glibmm-2.4` -o ./script 9 | // scriptisto-end 10 | 11 | int main(int argc, char *argv[]) { 12 | const auto user = Glib::getenv("USER"); 13 | std::cout << "Hello, C++! Current user: " << user << std::endl; 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /data/templates/dart_multifile.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | # This template is meant to compile whole Dart projects instead of a single file 3 | # 4 | # scriptisto-begin 5 | # script_src: dart_multifile_project/main.dart 6 | # build_in_script_dir: true 7 | # build_cmd: set -eu && dart compile exe "dart_multifile_project/main.dart" -o "${SCRIPTISTO_CACHE_DIR}/my-dart-program" 8 | # target_bin: my-dart-program 9 | # extra_src_paths: 10 | # - dart_multifile_project 11 | # scriptisto-end 12 | -------------------------------------------------------------------------------- /data/templates/sbcl.lisp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | ; scriptisto-begin 4 | ; script_src: script.lisp 5 | ; build_cmd: chmod +x build.lisp && ./build.lisp 6 | ; files: 7 | ; - path: build.lisp 8 | ; content: | 9 | ; #!/usr/bin/sbcl --script 10 | ; (load "script.lisp") 11 | ; (sb-ext:save-lisp-and-die "script" 12 | ; :executable t 13 | ; :toplevel 'main) 14 | ; scriptisto-end 15 | 16 | (defun main () 17 | (format t "Hello, Common Lisp!~%")) 18 | 19 | -------------------------------------------------------------------------------- /data/templates/go.go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | package main 4 | 5 | // scriptisto-begin 6 | // script_src: main.go 7 | // build_once_cmd: go mod tidy 8 | // build_cmd: go build -o script 9 | // replace_shebang_with: // 10 | // files: 11 | // - path: go.mod 12 | // content: | 13 | // module example.com/a/b 14 | // require ( 15 | // github.com/fatih/color v1.13.0 16 | // ) 17 | // scriptisto-end 18 | 19 | import "github.com/fatih/color" 20 | 21 | func main() { 22 | color.Yellow("Hello, Go!") 23 | } 24 | -------------------------------------------------------------------------------- /man/scriptisto-cache-info.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-cache-info 1 "scriptisto-cache-info " 4 | .SH NAME 5 | scriptisto\-cache\-info \- Shows information about the cache directory for the script 6 | .SH SYNOPSIS 7 | \fBscriptisto cache info\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIFILE\fR> 8 | .SH DESCRIPTION 9 | Shows information about the cache directory for the script 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fIFILE\fR> 16 | A filename of the script file. 17 | -------------------------------------------------------------------------------- /man/scriptisto-template-import.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-template-import 1 "scriptisto-template-import " 4 | .SH NAME 5 | scriptisto\-template\-import \- Imports a template from file 6 | .SH SYNOPSIS 7 | \fBscriptisto template import\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIFILE\fR> 8 | .SH DESCRIPTION 9 | Imports a template from file 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fIFILE\fR> 16 | A filename of the script file. Extension will be stripped for the template name. 17 | -------------------------------------------------------------------------------- /man/scriptisto-template-rm.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-template-rm 1 "scriptisto-template-rm " 4 | .SH NAME 5 | scriptisto\-template\-rm \- Remove a custom template or reset it to the built\-in contents 6 | .SH SYNOPSIS 7 | \fBscriptisto template rm\fR [\fB\-h\fR|\fB\-\-help\fR] <\fITEMPLATE_NAME\fR> 8 | .SH DESCRIPTION 9 | Remove a custom template or reset it to the built\-in contents 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fITEMPLATE_NAME\fR> 16 | A name of the template to remove 17 | -------------------------------------------------------------------------------- /data/templates/docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | # scriptisto-begin 4 | # script_src: internal.sh 5 | # build_cmd: docker build -t docker-script . && chmod +x ./external.sh 6 | # target_bin: ./external.sh 7 | # files: 8 | # - path: external.sh 9 | # content: | 10 | # #!/bin/bash 11 | # docker run -i --rm docker-script 12 | # - path: Dockerfile 13 | # content: | 14 | # FROM alpine 15 | # COPY internal.sh /internal.sh 16 | # CMD sh /internal.sh 17 | # scriptisto-end 18 | 19 | echo -n "Hello, Docker! Alpine Linux version: " 20 | cat /etc/alpine-release 21 | -------------------------------------------------------------------------------- /man/scriptisto-cache-get.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-cache-get 1 "scriptisto-cache-get " 4 | .SH NAME 5 | scriptisto\-cache\-get \- Shows a particular item from "info" by name 6 | .SH SYNOPSIS 7 | \fBscriptisto cache get\fR [\fB\-h\fR|\fB\-\-help\fR] <\fINAME\fR> <\fIFILE\fR> 8 | .SH DESCRIPTION 9 | Shows a particular item from "info" by name 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fINAME\fR> 16 | An item name, e.g. cache_path. 17 | .TP 18 | <\fIFILE\fR> 19 | A filename of the script file. 20 | -------------------------------------------------------------------------------- /man/scriptisto-template-edit.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-template-edit 1 "scriptisto-template-edit " 4 | .SH NAME 5 | scriptisto\-template\-edit \- Opens an editor to modify an existing template, nice for quick edits 6 | .SH SYNOPSIS 7 | \fBscriptisto template edit\fR [\fB\-h\fR|\fB\-\-help\fR] <\fITEMPLATE_NAME\fR> 8 | .SH DESCRIPTION 9 | Opens an editor to modify an existing template, nice for quick edits 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fITEMPLATE_NAME\fR> 16 | A name of the template to edit 17 | -------------------------------------------------------------------------------- /data/templates/elixir.ex: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | # scriptisto-begin 4 | # script_src: lib/script.ex 5 | # build_cmd: MIX_ENV=prod mix escript.build 6 | # files: 7 | # - path: mix.exs 8 | # content: | 9 | # defmodule Script.MixProject do 10 | # use Mix.Project 11 | # def project do 12 | # [ 13 | # app: :script, 14 | # version: "0.1.0", 15 | # elixir: "~> 1.8", 16 | # escript: [main_module: Script.CLI], 17 | # ] 18 | # end 19 | # end 20 | # scriptisto-end 21 | 22 | defmodule Script.CLI do 23 | def main(_) do 24 | IO.puts "Hello, Elixir!" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /data/templates/docker-c.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | #include 4 | #include 5 | 6 | // scriptisto-begin 7 | // script_src: main.c 8 | // build_cmd: clang -static -O2 main.c `pkg-config --libs --cflags glib-2.0` -o ./script 9 | // docker_build: 10 | // src_mount_dir: /src 11 | // dockerfile: | 12 | // FROM alpine 13 | // WORKDIR /src 14 | // RUN apk add glib-static glib-dev clang libc-dev build-base binutils pkgconfig 15 | // scriptisto-end 16 | 17 | int main(int argc, char* argv[]) { 18 | const gchar* user = g_getenv("USER"); 19 | printf("Hello, C built in Docker! Current user: %s\n", user); 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /man/scriptisto-new.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-new 1 "scriptisto-new " 4 | .SH NAME 5 | scriptisto\-new \- Prints an example starting script in a programming language of your choice 6 | .SH SYNOPSIS 7 | \fBscriptisto new\fR [\fB\-h\fR|\fB\-\-help\fR] [\fITEMPLATE_NAME\fR] 8 | .SH DESCRIPTION 9 | Prints an example starting script in a programming language of your choice 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | [\fITEMPLATE_NAME\fR] 16 | If specified, determines a language. Example usage: "scriptisto new | tee new\-script". 17 | If not specified, "new" lists available templates. 18 | -------------------------------------------------------------------------------- /data/templates/node.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: script.js 5 | // build_once_cmd: npm install 6 | // target_bin: ./script.js 7 | // target_interpreter: /usr/bin/env node 8 | // files: 9 | // - path: package.json 10 | // content: | 11 | // { "dependencies": { "yargs": "^14.2.0" } } 12 | // scriptisto-end 13 | 14 | argv = require('yargs') 15 | .usage('Usage: $0 [options]') 16 | .command('script', 'Example script') 17 | .alias('i', 'input').nargs('i', 1).describe('i', 'Example input') 18 | .help('h').alias('h', 'help') 19 | .argv; 20 | 21 | console.log("Hello, JavaScript on Node!. Input: " + (argv.input || "")); 22 | -------------------------------------------------------------------------------- /man/scriptisto-cache-clean.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-cache-clean 1 "scriptisto-cache-clean " 4 | .SH NAME 5 | scriptisto\-cache\-clean \- Clean the cache for a particular script. Removes the cache directory. Removes the Docker image/volume if they exist, but does not prune 6 | .SH SYNOPSIS 7 | \fBscriptisto cache clean\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIFILE\fR> 8 | .SH DESCRIPTION 9 | Clean the cache for a particular script. Removes the cache directory. Removes the Docker image/volume if they exist, but does not prune 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .TP 15 | <\fIFILE\fR> 16 | A filename of the script file. 17 | -------------------------------------------------------------------------------- /data/templates/python-mypy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | # scriptisto-begin 4 | # script_src: script.py 5 | # build_cmd: mypy script.py && python3 -m compileall . 6 | # target_interpreter: /usr/bin/env python3 7 | # target_bin: ./script.py 8 | # scriptisto-end 9 | 10 | import argparse 11 | from typing import Optional 12 | 13 | def print_hello(input: Optional[int]): 14 | msg = "Hello, Python! Input: %d" % (input or 0) 15 | print(msg) 16 | 17 | def main(): 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument("--input", type=int, help="Example input.") 20 | args = parser.parse_args() 21 | 22 | print_hello(args.input) 23 | 24 | if __name__== "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /man/scriptisto-build.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-build 1 "scriptisto-build " 4 | .SH NAME 5 | scriptisto\-build \- Build a script without running 6 | .SH SYNOPSIS 7 | \fBscriptisto build\fR [\fB\-b\fR|\fB\-\-build\-mode\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fISCRIPT_SRC\fR> 8 | .SH DESCRIPTION 9 | Build a script without running 10 | .SH OPTIONS 11 | .TP 12 | \fB\-b\fR, \fB\-\-build\-mode\fR=\fIBUILD_MODE\fR 13 | Build mode. If unset, only builds if necessary. "source" \- to rebuild each time. "full" to fully re\-fetch Docker image and run `build_once_cmd` 14 | .TP 15 | \fB\-h\fR, \fB\-\-help\fR 16 | Print help information 17 | .TP 18 | <\fISCRIPT_SRC\fR> 19 | A path to a script to build 20 | -------------------------------------------------------------------------------- /data/templates/docker-ocaml.ml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | (* 4 | scriptisto-begin 5 | script_src: script.ml 6 | build_cmd: cd /src/ && sudo chown -R $(id -u) /src && opam exec -- dune build script.exe 7 | target_bin: ./_build/default/script.exe 8 | docker_build: 9 | dockerfile: | 10 | FROM ocaml/opam2:alpine 11 | RUN sudo apk add m4 && opam install -y lwt 12 | src_mount_dir: /src 13 | files: 14 | - path: dune 15 | content: (executable (name script) (libraries lwt.unix)) 16 | - path: dune-workspace 17 | content: | 18 | (lang dune 1.1) 19 | (env (_ (flags -cclib -static))) 20 | scriptisto-end 21 | *) 22 | 23 | Lwt_main.run (Lwt_io.printf "Hello, OCaml!\n") 24 | 25 | -------------------------------------------------------------------------------- /data/templates/docker-reason.re: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | /* 4 | scriptisto-begin 5 | script_src: script.re 6 | build_cmd: cd /src/ && sudo chown -R $(id -u) /src && opam exec -- dune build script.exe 7 | target_bin: ./_build/default/script.exe 8 | docker_build: 9 | dockerfile: | 10 | FROM ocaml/opam2:alpine 11 | RUN sudo apk add m4 && opam install -y lwt reason 12 | src_mount_dir: /src 13 | files: 14 | - path: dune 15 | content: (executable (name script) (libraries lwt.unix)) 16 | - path: dune-workspace 17 | content: | 18 | (lang dune 1.1) 19 | (env (_ (flags -cclib -static))) 20 | scriptisto-end 21 | */ 22 | 23 | Lwt_main.run (Lwt_io.printf ("Hello, ReasonML!\n")); 24 | -------------------------------------------------------------------------------- /data/templates/docker-rust.rs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: src/main.rs 5 | // build_cmd: "cargo build --release && cp ./target/*musl*/release/script ./target/script" 6 | // target_bin: ./target/script 7 | // docker_build: 8 | // dockerfile: FROM clux/muslrust 9 | // src_mount_dir: /volume 10 | // extra_args: [-v,cargo-cache:/root/.cargo/registry] 11 | // files: 12 | // - path: Cargo.toml 13 | // content: | 14 | // package = { name = "script", version = "0.1.0", edition = "2018"} 15 | // [dependencies] 16 | // rand="*" 17 | // scriptisto-end 18 | 19 | fn main() { 20 | println!( 21 | "Hello, Rust built in Docker! Random: {}", 22 | rand::random::() 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /data/templates/rust.rs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: src/main.rs 5 | // build_cmd: cargo build --release && strip ./target/release/script 6 | // target_bin: ./target/release/script 7 | // files: 8 | // - path: Cargo.toml 9 | // content: | 10 | // package = { name = "script", version = "0.1.0", edition = "2018"} 11 | // [dependencies] 12 | // clap={version="4", features=["derive"]} 13 | // scriptisto-end 14 | 15 | use clap::Parser; 16 | 17 | #[derive(Debug, Parser)] 18 | #[command(name = "script", about = "A script.")] 19 | struct Opt { 20 | /// Example input 21 | #[arg(short, long)] 22 | input: Option, 23 | } 24 | 25 | fn main() { 26 | let opt = Opt::parse(); 27 | println!("Hello, Rust! Command line options: {:?}", opt); 28 | } 29 | -------------------------------------------------------------------------------- /man/scriptisto-cache.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-cache 1 "scriptisto-cache " 4 | .SH NAME 5 | scriptisto\-cache \- Build cache operations 6 | .SH SYNOPSIS 7 | \fBscriptisto cache\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIsubcommands\fR> 8 | .SH DESCRIPTION 9 | Build cache operations 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .SH SUBCOMMANDS 15 | .TP 16 | scriptisto\-cache\-info(1) 17 | Shows information about the cache directory for the script 18 | .TP 19 | scriptisto\-cache\-clean(1) 20 | Clean the cache for a particular script. Removes the cache directory. Removes the Docker image/volume if they exist, but does not prune 21 | .TP 22 | scriptisto\-cache\-get(1) 23 | Shows a particular item from "info" by name 24 | .TP 25 | scriptisto\-cache\-help(1) 26 | Print this message or the help of the given subcommand(s) 27 | -------------------------------------------------------------------------------- /man/scriptisto-template.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto-template 1 "scriptisto-template " 4 | .SH NAME 5 | scriptisto\-template \- Manage custom script templates 6 | .SH SYNOPSIS 7 | \fBscriptisto template\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIsubcommands\fR> 8 | .SH DESCRIPTION 9 | Manage custom script templates 10 | .SH OPTIONS 11 | .TP 12 | \fB\-h\fR, \fB\-\-help\fR 13 | Print help information 14 | .SH SUBCOMMANDS 15 | .TP 16 | scriptisto\-template\-import(1) 17 | Imports a template from file 18 | .TP 19 | scriptisto\-template\-edit(1) 20 | Opens an editor to modify an existing template, nice for quick edits 21 | .TP 22 | scriptisto\-template\-rm(1) 23 | Remove a custom template or reset it to the built\-in contents 24 | .TP 25 | scriptisto\-template\-ls(1) 26 | List all templates 27 | .TP 28 | scriptisto\-template\-help(1) 29 | Print this message or the help of the given subcommand(s) 30 | -------------------------------------------------------------------------------- /data/templates/csharp.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: Program.cs 5 | // target_bin: bin/Release/net8.0/script 6 | // build_cmd: dotnet build -c Release script.csproj 7 | // files: 8 | // - path: script.csproj 9 | // content: | 10 | // 11 | // 12 | // Exe 13 | // net8.0 14 | // enable 15 | // enable 16 | // 17 | // 18 | // scriptisto-end 19 | 20 | // See https://aka.ms/new-console-template for more information 21 | 22 | var name = Environment.GetEnvironmentVariable("USER"); 23 | Console.BackgroundColor = ConsoleColor.DarkBlue; 24 | Console.ForegroundColor = ConsoleColor.White; 25 | Console.Write("Hello"); 26 | Console.ResetColor(); 27 | Console.WriteLine($", {name}!"); 28 | -------------------------------------------------------------------------------- /data/templates/python-pip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | # scriptisto-begin 4 | # script_src: script.py 5 | # build_once_cmd: virtualenv -p python3 . && . ./bin/activate && pip install mypy termcolor 6 | # build_cmd: . ./bin/activate && mypy script.py && python3 -m compileall . && chmod +x ./run.sh 7 | # target_bin: ./run.sh 8 | # files: 9 | # - path: run.sh 10 | # content: | 11 | # #!/bin/sh 12 | # export DIR=$(dirname $0) 13 | # . $DIR/bin/activate 14 | # python3 $DIR/script.py $@ 15 | # scriptisto-end 16 | 17 | import argparse 18 | from termcolor import colored, cprint 19 | from typing import Optional 20 | 21 | def print_hello(input: Optional[int]): 22 | msg = "Hello, Python! Input: %d" % (input or 0) 23 | cprint(msg, 'green') 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument("--input", type=int, help="Example input.") 28 | args = parser.parse_args() 29 | 30 | print_hello(args.input) 31 | 32 | if __name__== "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /data/templates/java.java: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env scriptisto 2 | 3 | // scriptisto-begin 4 | // script_src: src/main/java/script/Script.java 5 | // build_cmd: gradle build && tar xf ./build/distributions/java.java.tar --strip 1 -C . 6 | // target_bin: ./bin/java.java 7 | // 8 | // files: 9 | // - path: build.gradle 10 | // content: | 11 | // apply plugin: 'java' 12 | // apply plugin: 'application' 13 | // mainClassName = 'script.Script' 14 | // tasks.distZip.enabled = false 15 | // repositories { 16 | // mavenCentral() 17 | // } 18 | // dependencies { 19 | // compile 'ch.qos.logback:logback-classic:1.2.3' 20 | // } 21 | // scriptisto-end 22 | 23 | package script; 24 | 25 | import java.util.Date; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | public class Script{ 30 | 31 | private static final Logger logger = LoggerFactory.getLogger(Script.class); 32 | 33 | public static void main(String[] args) { 34 | logger.debug("Hello, Java! Current Date : {}", new Date()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /man/scriptisto.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH scriptisto 1 "scriptisto " 4 | .SH NAME 5 | scriptisto \- A \*(Aqshebang\-interpreter\*(Aq for compiled languages 6 | .SH SYNOPSIS 7 | \fBscriptisto\fR [\fB\-h\fR|\fB\-\-help\fR] 8 | .TP 9 | \fBscriptisto\fR [\fISCRIPT\fR] 10 | .TP 11 | \fBscriptisto\fR [\fIsubcommands\fR] 12 | .SH DESCRIPTION 13 | A \*(Aqshebang\-interpreter\*(Aq for compiled languages 14 | .SH OPTIONS 15 | .TP 16 | \fB\-h\fR, \fB\-\-help\fR 17 | Print help information 18 | .TP 19 | [\fISCRIPT\fR] 20 | A path for to a script to run and additional arguments passed to this script. A script path must start with \*(Aq.\*(Aq or \*(Aq/\*(Aq 21 | .SH SUBCOMMANDS 22 | .TP 23 | scriptisto\-cache(1) 24 | Build cache operations 25 | .TP 26 | scriptisto\-new(1) 27 | Prints an example starting script in a programming language of your choice 28 | .TP 29 | scriptisto\-template(1) 30 | Manage custom script templates 31 | .TP 32 | scriptisto\-build(1) 33 | Build a script without running 34 | .TP 35 | scriptisto\-help(1) 36 | Print this message or the help of the given subcommand(s) 37 | -------------------------------------------------------------------------------- /.github/workflows/on-push.yml: -------------------------------------------------------------------------------- 1 | name: On Push 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | clippy_check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v2 19 | 20 | - name: Install stable toolchain 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: stable 25 | override: true 26 | components: clippy 27 | 28 | - name: Clippy check 29 | uses: actions-rs/clippy-check@v1 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | args: --all-features 33 | build-and-test: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v2 38 | 39 | - name: Install stable toolchain 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | profile: minimal 43 | toolchain: stable 44 | override: true 45 | components: clippy 46 | 47 | - name: Build 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: build 51 | args: --release 52 | 53 | - name: Test 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: test 57 | args: --release 58 | -------------------------------------------------------------------------------- /src/editor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::{Path, PathBuf}, 4 | process::Command, 5 | }; 6 | 7 | use anyhow::Context; 8 | 9 | use crate::common; 10 | 11 | pub fn edit>(source_file_path: P, content: &str) -> anyhow::Result> { 12 | use std::env::var; 13 | 14 | let tmp_file = { 15 | let mut path = env::temp_dir(); 16 | path.push("scriptisto"); 17 | path.push(format!("{}", std::process::id())); 18 | if !path.exists() { 19 | fs::create_dir_all(&path).with_context(|| format!("Unable to make dir: {:?}", path))? 20 | } 21 | let mut filename = String::from("script"); 22 | if let Some(ext) = source_file_path.as_ref().extension() { 23 | filename.push('.'); 24 | filename.push_str(&ext.to_string_lossy()); 25 | } 26 | path.push(filename); 27 | path 28 | }; 29 | log::info!("{:?}", tmp_file); 30 | 31 | common::write_bytes(&PathBuf::from("/"), &tmp_file, content.as_bytes())?; 32 | 33 | let editors = [ 34 | var("VISUAL"), 35 | var("EDITOR"), 36 | Ok("vim".into()), 37 | Ok("vi".into()), 38 | ]; 39 | for editor in editors.iter().flatten() { 40 | if Command::new(editor).arg(tmp_file.clone()).status().is_ok() { 41 | let content_after_editor = fs::read_to_string(tmp_file)?; 42 | if content_after_editor == content { 43 | return Ok(None); 44 | } else { 45 | return Ok(Some(content_after_editor)); 46 | } 47 | } 48 | } 49 | Err(anyhow::anyhow!( 50 | "Unable to open editor, specify valid editor via EDITOR or VISUAL environment variables" 51 | )) 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scriptisto 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/scriptisto.svg)](https://crates.io/crates/scriptisto) 4 | ![Build Status](https://github.com/igor-petruk/scriptisto/actions/workflows/on-push.yml/badge.svg) 5 | ![Crates.io License](https://img.shields.io/crates/l/scriptisto) 6 | ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/cargo/scriptisto) 7 | ![GitHub top language](https://img.shields.io/github/languages/top/igor-petruk/scriptisto) 8 | 9 | ![Crates.io](https://img.shields.io/crates/d/scriptisto?label=Cargo.io%20downloads) 10 | ![GitHub All Releases](https://img.shields.io/github/downloads/igor-petruk/scriptisto/total?logo=Github&label=Github%20Release%20downloads) 11 | 12 | [![Packaging status](https://repology.org/badge/vertical-allrepos/scriptisto.svg)](https://repology.org/project/scriptisto/versions) 13 | 14 | It is tool to enable writing one file scripts in languages that require compilation, dependencies fetching or preprocessing. 15 | 16 | It works as a "shebang" for those scripts, extracting build instructions from comments. If a script is changed, scriptisto rebuilds it and caches the result. If a script was already built, scriptisto immediately delegates to a binary with only <1 ms overhead. 17 | 18 | Builds in Docker without installing a compiler on a host system are [possible](https://github.com/igor-petruk/scriptisto/wiki/Writing-scripts#builds-in-docker). 19 | 20 | Advantages and use-cases are listed in the [Wiki](https://github.com/igor-petruk/scriptisto/wiki#advantages). 21 | 22 | ## Demo 23 | 24 | ```c 25 | #!/usr/bin/env scriptisto 26 | 27 | #include 28 | #include 29 | 30 | // scriptisto-begin 31 | // script_src: main.c 32 | // build_cmd: clang -O2 main.c `pkg-config --libs --cflags glib-2.0` -o ./script 33 | // scriptisto-end 34 | 35 | int main(int argc, char *argv[]) { 36 | gchar* user = g_getenv("USER"); 37 | printf("Hello, C! Current user: %s\n", user); 38 | return 0; 39 | } 40 | ``` 41 | 42 | ```shell 43 | $ chmod +x ./script.c 44 | $ ./script.c 45 | Hello, C! Current user: username 46 | ``` 47 | 48 | **Note:** some templates such as `rust` take more time to build during the first time. The default behaviour is to supress the build logs, so do not be discouraged that you do not immediately see any output. More info in the [wiki](https://github.com/igor-petruk/scriptisto/wiki/Running-scripts#build-logs). 49 | 50 | ## Installation 51 | 52 | Scriptisto is available as a prebuilt statically-linked standalone binary or distributions packages at [Releases](https://github.com/igor-petruk/scriptisto/releases) or at [Crates.io](https://crates.io/crates/scriptisto). 53 | 54 | Please proceed to the [Installation](https://github.com/igor-petruk/scriptisto/wiki/Installation) for instructions. 55 | 56 | ## Documentation 57 | 58 | Proceed to our [Wiki](https://github.com/igor-petruk/scriptisto/wiki). 59 | 60 | ## Disclaimer 61 | 62 | This is not an officially supported Google product. 63 | -------------------------------------------------------------------------------- /.github/workflows/on-release.yml: -------------------------------------------------------------------------------- 1 | name: On Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*.*.*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | release: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | build: [linux, macos] 18 | include: 19 | - build: linux 20 | os: ubuntu-22.04 21 | rust: stable 22 | target: x86_64-unknown-linux-musl 23 | - build: macos 24 | os: macos-12 25 | rust: stable 26 | target: x86_64-apple-darwin 27 | steps: 28 | - name: Install Ubuntu tools 29 | if: matrix.os == 'ubuntu-22.04' 30 | run: sudo apt-get update && sudo apt-get install -y musl-tools rpm 31 | 32 | - name: Install Mac OS X tools 33 | if: matrix.build == 'macos' 34 | run: brew install coreutils 35 | 36 | - name: Checkout sources 37 | uses: actions/checkout@v2 38 | 39 | - name: Install toolchain 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: ${{ matrix.rust }} 43 | target: ${{ matrix.target }} 44 | override: true 45 | 46 | - name: Build binary 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: build 50 | args: --release --target ${{ matrix.target }} 51 | 52 | - name: Strip binary 53 | run: strip ./target/${{ matrix.target }}/release/scriptisto 54 | 55 | - name: Make bz2 56 | run: | 57 | tar -cjvf ./scriptisto-${{ matrix.target }}.tar.bz2 --directory=./target/${{ matrix.target }}/release scriptisto 58 | sha256sum ./scriptisto-${{ matrix.target }}.tar.bz2 59 | 60 | - name: Install tools from crates 61 | if: matrix.build == 'linux' 62 | uses: actions-rs/cargo@v1 63 | with: 64 | command: install 65 | args: cargo-deb cargo-generate-rpm 66 | 67 | - name: Copy to standard location 68 | if: matrix.build == 'linux' 69 | run: | 70 | mkdir -p ./target/release 71 | cp ./target/${{ matrix.target }}/release/scriptisto ./target/release/scriptisto 72 | 73 | - name: Build RPM 74 | if: matrix.build == 'linux' 75 | run: | 76 | cargo generate-rpm --target ${{ matrix.target }} 77 | sha256sum ./target/${{ matrix.target }}/generate-rpm/*.rpm 78 | 79 | - name: Build DEB 80 | if: matrix.build == 'linux' 81 | run: | 82 | cargo deb --no-build --separate-debug-symbols --target ${{ matrix.target }} 83 | sha256sum ./target/${{ matrix.target }}/debian/*.deb 84 | 85 | - name: Release Linux 86 | uses: softprops/action-gh-release@v1 87 | if: matrix.build == 'linux' && startsWith(github.ref, 'refs/tags/') 88 | with: 89 | append_body: true 90 | files: | 91 | ./scriptisto-${{ matrix.target }}.tar.bz2 92 | ./target/${{ matrix.target }}/debian/*.deb 93 | ./target/${{ matrix.target }}/generate-rpm/*.rpm 94 | 95 | - name: Release Mac OS X 96 | uses: softprops/action-gh-release@v1 97 | if: matrix.build == 'macos' && startsWith(github.ref, 'refs/tags/') 98 | with: 99 | append_body: true 100 | files: | 101 | ./scriptisto-${{ matrix.target }}.tar.bz2 102 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::{anyhow, Context, Result}; 16 | use log::debug; 17 | use std::fs::File; 18 | use std::io::Write; 19 | use std::path::{Path, PathBuf}; 20 | use std::process::{Command, Stdio}; 21 | 22 | pub fn script_src_to_absolute(script_src: &Path) -> Result { 23 | let script_src_str = script_src.to_string_lossy(); 24 | if !script_src_str.starts_with(['.', '/']) { 25 | return Err(anyhow!( 26 | "Script path {:?} must start with '.' or '/'", 27 | script_src 28 | )); 29 | } 30 | Ok(script_src.canonicalize()?) 31 | } 32 | 33 | pub fn build_cache_path(script_path: &Path) -> Result { 34 | let script_path = script_src_to_absolute(script_path)?; 35 | let script_path_rel = script_path 36 | .strip_prefix("/") 37 | .context(format!("Could not strip '/' prefix from {:?}", script_path))?; 38 | 39 | let mut user_cache = 40 | dirs::cache_dir().ok_or_else(|| anyhow!("Cannot compute user's cache dir"))?; 41 | user_cache.push("scriptisto/bin"); 42 | user_cache.push(script_path_rel); 43 | Ok(user_cache) 44 | } 45 | 46 | pub fn write_bytes(cache_path: &Path, rel_path: &Path, data: &[u8]) -> Result<()> { 47 | let mut path = cache_path.to_path_buf(); 48 | path.push(rel_path); 49 | debug!("Writing {} bytes to {:?}", data.len(), path); 50 | let parent = path 51 | .parent() 52 | .ok_or_else(|| anyhow!("Cannot compute parent path of {:?}", path))?; 53 | std::fs::create_dir_all(parent).context(format!( 54 | "Cannot create cache directory for script, dir path: {:?}", 55 | parent 56 | ))?; 57 | let mut file = File::create(path).context("Cannot output extra file")?; 58 | file.write_all(data).context("Cannot write bytes to file")?; 59 | Ok(()) 60 | } 61 | 62 | pub fn file_modified(p: &Path) -> Result { 63 | let meta = std::fs::metadata(p)?; 64 | let modified = meta.modified()?; 65 | Ok(modified) 66 | } 67 | 68 | pub fn run_command( 69 | current_directory: &Path, 70 | mut cmd: Command, 71 | stderr_mode: Stdio, 72 | ) -> Result { 73 | cmd.stdout(Stdio::piped()) 74 | .stderr(stderr_mode) 75 | .current_dir(current_directory); 76 | 77 | debug!("Running command: {:?}", cmd); 78 | 79 | let out = cmd.output().context(format!( 80 | "Cannot run cmd={:?} in current_directory={:?}", 81 | cmd, current_directory 82 | ))?; 83 | 84 | let stderr = String::from_utf8_lossy(&out.stderr); 85 | let stdout = String::from_utf8_lossy(&out.stdout); 86 | debug!( 87 | "Command result: {:?}\nstderr:\n{}\nstdout:\n{}", 88 | out.status.code(), 89 | stderr, 90 | stdout 91 | ); 92 | 93 | if !out.status.success() { 94 | eprintln!("{}", stderr); 95 | eprintln!("{}", stdout); 96 | let error = match out.status.code() { 97 | Some(code) => anyhow!("Command {:?} failed. Exit code: {}.", cmd, code,), 98 | None => anyhow!("Child build process terminated by signal"), 99 | }; 100 | return Err(error); 101 | } 102 | 103 | Ok(out) 104 | } 105 | -------------------------------------------------------------------------------- /src/cfg.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use anyhow::{Context, Result}; 16 | use log::debug; 17 | use serde_derive::Deserialize; 18 | use std::cmp::min; 19 | use std::io::{BufRead, BufReader}; 20 | 21 | #[derive(Deserialize, Debug)] 22 | pub struct BuildSpec { 23 | pub script_src: String, 24 | pub build_cmd: Option, 25 | pub build_once_cmd: Option, 26 | #[serde(default = "default_target_bin")] 27 | pub target_bin: String, 28 | pub target_interpreter: Option, 29 | #[serde(default)] 30 | pub replace_shebang_with: String, 31 | #[serde(default)] 32 | pub files: Vec, 33 | #[serde(default)] 34 | pub docker_build: Option, 35 | #[serde(default)] 36 | pub extra_src_paths: Vec, // paths to directory/file, no wildcards supported 37 | #[serde(default)] 38 | pub build_in_script_dir: bool, // use script directory as working directory of build, not the cache directory (non-Docker build only) 39 | } 40 | 41 | fn default_target_bin() -> String { 42 | "./script".into() 43 | } 44 | 45 | #[derive(Deserialize, Debug)] 46 | pub struct File { 47 | pub path: String, 48 | pub content: String, 49 | } 50 | 51 | #[derive(Deserialize, Debug)] 52 | pub struct DockerBuild { 53 | pub dockerfile: Option, 54 | pub src_mount_dir: Option, 55 | #[serde(default)] 56 | pub extra_args: Vec, 57 | } 58 | 59 | #[derive(Clone, Debug)] 60 | enum ParserState { 61 | ScriptSource, 62 | ConfigSource { prefix_len: usize }, 63 | } 64 | 65 | impl BuildSpec { 66 | pub fn new(script_body: &[u8]) -> Result { 67 | let mut script_src = Vec::new(); 68 | let reader = BufReader::new(script_body); 69 | 70 | use ParserState::*; 71 | let mut state = ParserState::ScriptSource; 72 | 73 | let mut cfg_src = vec![]; 74 | 75 | for (line_num, line) in reader.lines().enumerate() { 76 | let mut line = line.context(format!("Cannot parse script line: {}", line_num))?; 77 | script_src.push(line.clone()); 78 | state = match state { 79 | ScriptSource => { 80 | let sb_start = line.find("scriptisto-begin"); 81 | if let Some(pos) = sb_start { 82 | ConfigSource { prefix_len: pos } 83 | } else { 84 | state 85 | } 86 | } 87 | ConfigSource { prefix_len } => { 88 | line.drain(..min(prefix_len, line.len())); 89 | if line.starts_with("scriptisto-end") { 90 | ScriptSource 91 | } else { 92 | cfg_src.push(line); 93 | state 94 | } 95 | } 96 | }; 97 | } 98 | 99 | let mut build_spec: BuildSpec = serde_yaml::from_str(&cfg_src.join("\n")) 100 | .context(format!("Cannot parse config YAML: \n{:#?}", cfg_src))?; 101 | 102 | let replace_shebang_with = build_spec.replace_shebang_with.clone(); 103 | if !script_src.is_empty() { 104 | script_src[0] = replace_shebang_with; 105 | } 106 | 107 | build_spec.files.push(File { 108 | path: build_spec.script_src.clone(), 109 | content: script_src.join("\n"), 110 | }); 111 | 112 | debug!("BuildSpec parsed: {:#?}", build_spec); 113 | 114 | Ok(build_spec) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::opt::CacheCommand; 16 | use anyhow::{anyhow, Context, Result}; 17 | use number_prefix::NumberPrefix; 18 | use std::collections::BTreeMap; 19 | use std::path::{Path, PathBuf}; 20 | use std::process; 21 | 22 | use crate::*; 23 | 24 | fn print_item(name: &str, value: &str) { 25 | println!("{:20} {}", format!("{}:", name), value); 26 | } 27 | 28 | fn get_dir_size_lossy(path: &Path) -> String { 29 | let size: u64 = walkdir::WalkDir::new(path) 30 | .into_iter() 31 | .map(|r| { 32 | r.map(|e| e.metadata().map(|m| m.len()).unwrap_or_default()) 33 | .unwrap_or_default() 34 | }) 35 | .sum(); 36 | 37 | match NumberPrefix::binary(size as f64) { 38 | NumberPrefix::Standalone(bytes) => format!("{} bytes", bytes), 39 | NumberPrefix::Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix), 40 | } 41 | } 42 | 43 | fn collect_info(script_path: &Path) -> Result> { 44 | let script_body = std::fs::read(script_path).context("Cannot read script file")?; 45 | let script_cache_path = common::build_cache_path(script_path).context(format!( 46 | "Cannot build cache path for script: {:?}", 47 | script_path 48 | ))?; 49 | let cfg = cfg::BuildSpec::new(&script_body)?; 50 | 51 | let mut items = BTreeMap::new(); 52 | 53 | items.insert( 54 | "cache_path".into(), 55 | script_cache_path.to_string_lossy().to_string(), 56 | ); 57 | 58 | if cfg.docker_build.is_some() { 59 | items.insert( 60 | "docker_image".into(), 61 | build::docker_image_name(&script_cache_path)?, 62 | ); 63 | items.insert( 64 | "docker_src_volume".into(), 65 | build::docker_volume_name(&script_cache_path)?, 66 | ); 67 | items.insert("dir_size".into(), get_dir_size_lossy(&script_cache_path)); 68 | } 69 | 70 | Ok(items) 71 | } 72 | 73 | pub fn command_get(name: &str, script_path: &Path) -> Result<()> { 74 | let items = collect_info(script_path)?; 75 | 76 | if let Some(value) = items.get(name) { 77 | println!("{}", value); 78 | Ok(()) 79 | } else { 80 | Err(anyhow!( 81 | "'{}' is not found. Available items: {:?}.", 82 | name, 83 | items.keys() 84 | )) 85 | } 86 | } 87 | 88 | pub fn command_info(script_path: &Path) -> Result<()> { 89 | let items = collect_info(script_path)?; 90 | 91 | for (k, v) in items.iter() { 92 | print_item(k, v); 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | pub fn command_clean(script_path: &Path) -> Result<()> { 99 | let items = collect_info(script_path)?; 100 | let cache_path = items.get("cache_path").expect("cache_path to exist"); 101 | 102 | let _ = std::fs::remove_dir_all(cache_path); 103 | 104 | if let Some(docker_image) = items.get("docker_image") { 105 | let mut cmd = process::Command::new("docker"); 106 | cmd.arg("image").arg("rm").arg(docker_image); 107 | let _ = common::run_command(&PathBuf::from("/"), cmd, process::Stdio::piped()); 108 | } 109 | 110 | if let Some(docker_volume) = items.get("docker_src_volume") { 111 | let mut cmd = process::Command::new("docker"); 112 | cmd.arg("volume").arg("rm").arg(docker_volume); 113 | let _ = common::run_command(&PathBuf::from("/"), cmd, process::Stdio::piped()); 114 | } 115 | 116 | Ok(()) 117 | } 118 | 119 | pub fn command_cache(cmd: CacheCommand) -> Result<()> { 120 | match cmd { 121 | CacheCommand::Clean { file } => command_clean(&file), 122 | CacheCommand::Get { name, file } => command_get(&name, &file), 123 | CacheCommand::Info { file } => command_info(&file), 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Scriptisto Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "scriptisto" 17 | version = "2.2.0" 18 | edition = "2018" 19 | license = "Apache-2.0" 20 | description = "A language-agnostic \"shebang interpreter\" that enables you to write scripts in compiled languages." 21 | repository = "https://github.com/igor-petruk/scriptisto" 22 | readme = "README.md" 23 | categories = ["development-tools::build-utils", "command-line-utilities"] 24 | authors = [ 25 | "Google LLC", 26 | "Igor Petruk", 27 | ] 28 | 29 | [package.metadata.rpm.cargo] 30 | target = "x86_64-unknown-linux-musl" 31 | buildflags = ["--release"] 32 | 33 | [package.metadata.rpm.targets] 34 | scriptisto = { path = "/usr/bin/scriptisto" } 35 | 36 | [dependencies] 37 | dirs = '5' 38 | exec = '0.3' 39 | anyhow = '1' 40 | include_dir = '0.7' 41 | log = '0.4' 42 | serde='1.0' 43 | serde_derive='1.0' 44 | serde_yaml='0.9' 45 | md5="0" 46 | prettytable-rs="0.10" 47 | uzers="0.12" 48 | walkdir="2" 49 | number_prefix="0.4" 50 | clap = { version = "3", features = ["derive"] } 51 | 52 | [dependencies.env_logger] 53 | default-features = false 54 | version = '0' 55 | 56 | [package.metadata.deb] 57 | section = "utils" 58 | assets = [ 59 | ["target/release/scriptisto", "usr/bin/", "755"], 60 | ["LICENSE", "usr/share/doc/scriptisto/", "644"], 61 | ["README.md", "usr/share/doc/scriptisto/README", "644"], 62 | ["man/scriptisto.1", "usr/share/man/man1/scriptisto.1", "644"], 63 | ["man/scriptisto-build.1", "usr/share/man/man1/scriptisto-build.1", "644"], 64 | ["man/scriptisto-cache.1", "usr/share/man/man1/scriptisto-cache.1", "644"], 65 | ["man/scriptisto-cache-clean.1", "usr/share/man/man1/scriptisto-cache-clean.1", "644"], 66 | ["man/scriptisto-cache-get.1", "usr/share/man/man1/scriptisto-cache-get.1", "644"], 67 | ["man/scriptisto-cache-info.1", "usr/share/man/man1/scriptisto-cache-info.1", "644"], 68 | ["man/scriptisto-new.1", "usr/share/man/man1/scriptisto-new.1", "644"], 69 | ["man/scriptisto-template.1", "usr/share/man/man1/scriptisto-template.1", "644"], 70 | ["man/scriptisto-template-edit.1", "usr/share/man/man1/scriptisto-template-edit.1", "644"], 71 | ["man/scriptisto-template-import.1", "usr/share/man/man1/scriptisto-template-import.1", "644"], 72 | ["man/scriptisto-template-ls.1", "usr/share/man/man1/scriptisto-template-ls.1", "644"], 73 | ["man/scriptisto-template-rm.1", "usr/share/man/man1/scriptisto-template-rm.1", "644"], 74 | ] 75 | 76 | [package.metadata.generate-rpm] 77 | assets = [ 78 | {source="target/release/scriptisto", dest="/usr/bin/",mode= "755"}, 79 | {source="LICENSE", dest="/usr/share/doc/scriptisto/LICENSE", mode="644"}, 80 | {source="README.md", dest="/usr/share/doc/scriptisto/README.md", mode="644"}, 81 | {source="man/scriptisto.1", dest="/usr/share/man/man1/scriptisto.1", mode="644"}, 82 | {source="man/scriptisto-build.1", dest="/usr/share/man/man1/scriptisto-build.1", mode="644"}, 83 | {source="man/scriptisto-cache.1", dest="/usr/share/man/man1/scriptisto-cache.1", mode="644"}, 84 | {source="man/scriptisto-cache-clean.1", dest="/usr/share/man/man1/scriptisto-cache-clean.1", mode="644"}, 85 | {source="man/scriptisto-cache-get.1", dest="/usr/share/man/man1/scriptisto-cache-get.1", mode="644"}, 86 | {source="man/scriptisto-cache-info.1", dest="/usr/share/man/man1/scriptisto-cache-info.1", mode="644"}, 87 | {source="man/scriptisto-new.1", dest="/usr/share/man/man1/scriptisto-new.1", mode="644"}, 88 | {source="man/scriptisto-template.1", dest="/usr/share/man/man1/scriptisto-template.1", mode="644"}, 89 | {source="man/scriptisto-template-edit.1", dest="/usr/share/man/man1/scriptisto-template-edit.1", mode="644"}, 90 | {source="man/scriptisto-template-import.1", dest="/usr/share/man/man1/scriptisto-template-import.1", mode="644"}, 91 | {source="man/scriptisto-template-ls.1", dest="/usr/share/man/man1/scriptisto-template-ls.1", mode="644"}, 92 | {source="man/scriptisto-template-rm.1", dest="/usr/share/man/man1/scriptisto-template-rm.1", mode="644"}, 93 | ] 94 | 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *Igor Petruk *, the 73 | Project Steward(s) for *Scriptisto*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | 95 | -------------------------------------------------------------------------------- /src/opt.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use clap::{Parser, Subcommand}; 16 | use std::path::PathBuf; 17 | use std::str::FromStr; 18 | 19 | #[derive(Debug, Parser, PartialEq, Eq)] 20 | pub enum CacheCommand { 21 | /// Shows information about the cache directory for the script. 22 | Info { 23 | #[clap(help = "A filename of the script file.")] 24 | file: PathBuf, 25 | }, 26 | /// Clean the cache for a particular script. Removes the cache directory. Removes the Docker image/volume if 27 | /// they exist, but does not prune. 28 | #[clap(visible_alias = "clear")] 29 | Clean { 30 | #[clap(help = "A filename of the script file.")] 31 | file: PathBuf, 32 | }, 33 | /// Shows a particular item from "info" by name. 34 | Get { 35 | #[clap(help = "An item name, e.g. cache_path.")] 36 | name: String, 37 | #[clap(help = "A filename of the script file.")] 38 | file: PathBuf, 39 | }, 40 | } 41 | 42 | #[derive(Debug, Parser, PartialEq, Eq)] 43 | pub enum TemplatesCommand { 44 | /// Imports a template from file. 45 | Import { 46 | #[clap( 47 | help = "A filename of the script file. Extension will be stripped for the template name." 48 | )] 49 | file: PathBuf, 50 | }, 51 | /// Opens an editor to modify an existing template, nice for quick edits. 52 | Edit { 53 | #[clap(help = "A name of the template to edit")] 54 | template_name: String, 55 | }, 56 | /// Remove a custom template or reset it to the built-in contents. 57 | #[clap(name = "rm", visible_aliases = &["remove", "delete"])] 58 | Remove { 59 | #[clap(help = "A name of the template to remove")] 60 | template_name: String, 61 | }, 62 | /// List all templates. 63 | #[clap(name = "ls", visible_alias = "list")] 64 | List {}, 65 | } 66 | 67 | #[derive(Debug, PartialEq, Eq, Parser, Clone, Default)] 68 | #[clap(rename_all = "snake-case")] 69 | pub enum BuildMode { 70 | #[default] 71 | Default, 72 | Source, 73 | Full, 74 | } 75 | 76 | impl FromStr for BuildMode { 77 | type Err = anyhow::Error; 78 | 79 | fn from_str(s: &str) -> anyhow::Result { 80 | use BuildMode::*; 81 | Ok(match s { 82 | "" => Default, 83 | "source" => Source, 84 | "full" => Full, 85 | _ => { 86 | return Err(anyhow::anyhow!( 87 | "Incorrect build mode value. Available values: , source, full." 88 | )) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | #[derive(Debug, Parser, PartialEq, Eq)] 95 | #[clap( 96 | name = "scriptisto", 97 | about = "A 'shebang-interpreter' for compiled languages", 98 | args_conflicts_with_subcommands = true 99 | )] 100 | pub struct Opt { 101 | /// A path for to a script to run and additional arguments passed to this script. A script path must start with '.' or '/'. 102 | #[clap(value_name = "SCRIPT")] 103 | pub command: Vec, 104 | 105 | #[clap(subcommand)] 106 | pub cmd: Option, 107 | } 108 | 109 | #[derive(Debug, PartialEq, Subcommand, Eq)] 110 | pub enum Command { 111 | /// Build cache operations. 112 | Cache { 113 | #[clap(subcommand)] 114 | cmd: CacheCommand, 115 | }, 116 | /// Prints an example starting script in a programming language of your 117 | /// choice. 118 | New { 119 | #[clap( 120 | help = "If specified, determines a language. Example usage: \"scriptisto new | tee new-script\".\nIf not specified, \"new\" lists available templates." 121 | )] 122 | template_name: Option, 123 | }, 124 | /// Manage custom script templates. 125 | Template { 126 | #[clap(subcommand)] 127 | cmd: TemplatesCommand, 128 | }, 129 | /// Build a script without running. 130 | Build { 131 | /// A path to a script to build. 132 | #[clap()] 133 | script_src: String, 134 | /// Build mode. If unset, only builds if necessary. "source" - to rebuild each time. "full" to fully re-fetch Docker image and run `build_once_cmd`. 135 | #[clap(short, long)] 136 | build_mode: Option, 137 | }, 138 | } 139 | 140 | pub fn display_help() { 141 | Opt::parse_from(vec!["", "help"]); 142 | } 143 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[macro_use] 16 | extern crate include_dir; 17 | 18 | use anyhow::{anyhow, Context, Result}; 19 | use clap::Parser; 20 | use log::debug; 21 | use std::env; 22 | use std::path::{Path, PathBuf}; 23 | use std::process::exit; 24 | use std::str::FromStr; 25 | 26 | mod build; 27 | mod cache; 28 | mod cfg; 29 | mod common; 30 | mod editor; 31 | mod opt; 32 | mod templates; 33 | 34 | pub fn opt_from_args(args: &[String]) -> opt::Opt { 35 | let mut args_iter = args.iter(); 36 | args_iter.next(); 37 | 38 | if let Some(script_src) = args_iter.next() { 39 | let absolute_script_src = common::script_src_to_absolute(Path::new(&script_src)); 40 | if let Ok(absolute_script_src) = absolute_script_src { 41 | if absolute_script_src.exists() { 42 | let mut command: Vec = vec![absolute_script_src.to_string_lossy().into()]; 43 | command.append(&mut args_iter.cloned().collect()); 44 | return opt::Opt { command, cmd: None }; 45 | } 46 | } 47 | } 48 | 49 | let opts = opt::Opt::parse_from(args.iter()); 50 | debug!("Parsed command line options: {:#?}", opts); 51 | 52 | if opts.cmd.is_none() && opts.command.is_empty() { 53 | opt::display_help(); 54 | }; 55 | opts 56 | } 57 | 58 | fn default_main(script_path: &str, args: &[String]) -> Result<()> { 59 | let build_mode_env = std::env::var_os("SCRIPTISTO_BUILD").unwrap_or_default(); 60 | let build_mode = opt::BuildMode::from_str(&build_mode_env.to_string_lossy())?; 61 | let show_logs = std::env::var_os("SCRIPTISTO_BUILD_LOGS").is_some(); 62 | 63 | let (cfg, script_cache_path) = build::perform(build_mode, script_path, show_logs) 64 | .context(format!("Build failed for {:?}", script_path))?; 65 | 66 | let mut full_target_bin = script_cache_path.clone(); 67 | full_target_bin.push(PathBuf::from(cfg.target_bin)); 68 | let full_target_bin = full_target_bin 69 | .canonicalize()? 70 | .to_string_lossy() 71 | .to_string(); 72 | debug!("Full target_bin path: {:?}", full_target_bin); 73 | 74 | let (binary, mut target_argv) = match cfg.target_interpreter { 75 | Some(ref target_interpreter) if !target_interpreter.is_empty() => { 76 | let mut seq: Vec = target_interpreter 77 | .split_ascii_whitespace() 78 | .map(|s| s.to_string()) 79 | .collect(); 80 | let binary = seq 81 | .first() 82 | .expect("first() should work as we checked the guard above") 83 | .clone(); 84 | seq.drain(..1); 85 | seq.push(full_target_bin); 86 | (binary, seq) 87 | } 88 | _ => (full_target_bin, vec![]), 89 | }; 90 | target_argv.insert(0, binary.clone()); 91 | // args.drain(..2); 92 | target_argv.extend_from_slice(args); 93 | debug!("Running exec {:?}, Args: {:?}", binary, target_argv); 94 | 95 | // Scripts can use this to find other build artifacts 96 | env::set_var(build::SCRIPTISTO_CACHE_DIR_VAR, script_cache_path); 97 | 98 | let error = match exec::execvp(&binary, &target_argv) { 99 | exec::Error::Errno(e) => { 100 | anyhow!("Cannot execute target binary '{:?}': {:#?}", binary, e) 101 | } 102 | _ => anyhow!("Cannot exec"), 103 | }; 104 | Err(error) 105 | } 106 | 107 | fn main_err() -> Result<()> { 108 | let args: Vec = std::env::args().collect(); 109 | let opts = opt_from_args(&args); 110 | debug!("Parsed options: {:?}", opts); 111 | 112 | match opts.cmd { 113 | None => { 114 | let mut command_iter = opts.command.iter(); 115 | let script_src = command_iter.next().ok_or_else(|| { 116 | anyhow!("PROBABLY A BUG: script_src must be non-empty if no subcommand found.") 117 | })?; 118 | let script_src = common::script_src_to_absolute(Path::new(&script_src))?; 119 | let args: Vec = command_iter.cloned().collect(); 120 | default_main(&script_src.to_string_lossy(), args.as_slice()) 121 | } 122 | Some(opt::Command::Cache { cmd }) => cache::command_cache(cmd), 123 | Some(opt::Command::New { template_name }) => templates::command_new(template_name), 124 | Some(opt::Command::Template { cmd }) => templates::command_template(cmd), 125 | Some(opt::Command::Build { 126 | script_src, 127 | build_mode, 128 | }) => { 129 | let _ = build::perform(build_mode.unwrap_or_default(), &script_src, true); 130 | Ok(()) 131 | } 132 | } 133 | } 134 | 135 | fn main() { 136 | env_logger::init(); 137 | 138 | if let Err(e) = main_err() { 139 | eprintln!("Error: {:?}", e); 140 | exit(1); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/templates.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Scriptisto Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::editor; 16 | use crate::opt::TemplatesCommand; 17 | use anyhow::{anyhow, Context, Result}; 18 | use include_dir::Dir; 19 | use log::debug; 20 | use std::collections::BTreeMap; 21 | use std::fmt::Debug; 22 | use std::fs::File; 23 | use std::io::Write; 24 | use std::path::{Path, PathBuf}; 25 | 26 | const TEMPLATES: Dir = include_dir!("./data/templates/"); 27 | 28 | #[derive(Debug, PartialEq, Eq)] 29 | enum Source { 30 | BuiltIn, 31 | Custom, 32 | } 33 | 34 | #[derive(Debug)] 35 | struct Template { 36 | source: Source, 37 | filename: String, 38 | contents: String, 39 | } 40 | 41 | type TemplateMap = BTreeMap; 42 | 43 | fn path_to_file_name + Debug>(p: T) -> Result { 44 | let p: PathBuf = p.as_ref().into(); 45 | Ok(p.file_name() 46 | .ok_or_else(|| anyhow!("Cannot extract filename from {:?}", p))? 47 | .to_string_lossy() 48 | .to_string()) 49 | } 50 | 51 | fn filename_to_template_name>(p: T) -> Result { 52 | let p: PathBuf = p.as_ref().into(); 53 | let file_name = path_to_file_name(&p)?; 54 | Ok(p.file_stem() 55 | .map(|f| f.to_string_lossy().to_string()) 56 | .unwrap_or(file_name)) 57 | } 58 | 59 | // Also creates the directory, this ok for now. 60 | fn filename_to_template_path + Debug>(p: T) -> Result { 61 | let file_name = path_to_file_name(&p)?; 62 | let templates_directory = get_templates_directory()?; 63 | std::fs::create_dir_all(&templates_directory)?; 64 | let mut template_path = templates_directory; 65 | template_path.push(file_name); 66 | Ok(template_path) 67 | } 68 | 69 | fn get_built_in_templates() -> Result { 70 | let mut templates = TemplateMap::new(); 71 | for file in TEMPLATES.files() { 72 | let path = PathBuf::from(file.path()); 73 | templates.insert( 74 | filename_to_template_name(&path)?, 75 | Template { 76 | source: Source::BuiltIn, 77 | filename: path_to_file_name(path)?, 78 | contents: file 79 | .contents_utf8() 80 | .ok_or_else(|| anyhow!("File {:?} is not UTF-8", file))? 81 | .to_string(), 82 | }, 83 | ); 84 | } 85 | Ok(templates) 86 | } 87 | 88 | fn get_templates_directory() -> Result { 89 | let mut p = dirs::config_dir().ok_or_else(|| anyhow!("Cannot compute user's config dir"))?; 90 | p.push("scriptisto/templates"); 91 | Ok(p) 92 | } 93 | 94 | fn get_custom_templates() -> Result { 95 | let mut templates = TemplateMap::new(); 96 | 97 | let templates_dir = get_templates_directory()?; 98 | 99 | debug!("Scanning for custom templates at {:?};", templates_dir); 100 | match std::fs::read_dir(&templates_dir) { 101 | Ok(dir_iter) => { 102 | debug!("Custom templates directory found"); 103 | for template_file in dir_iter { 104 | let template_file = template_file?; 105 | let name = filename_to_template_name(template_file.path())?; 106 | let filename = path_to_file_name(template_file.path())?; 107 | let contents = std::fs::read_to_string(template_file.path())?; 108 | templates.insert( 109 | name, 110 | Template { 111 | source: Source::Custom, 112 | filename, 113 | contents, 114 | }, 115 | ); 116 | } 117 | } 118 | Err(e) => { 119 | debug!("The custom templates directory skipped, reason: {:?}.", e); 120 | } 121 | } 122 | 123 | Ok(templates) 124 | } 125 | 126 | fn print_ascii_table(rows: &[Vec]) { 127 | use prettytable::{format, row, Cell, Row, Table}; 128 | 129 | let mut table = Table::new(); 130 | let format = format::FormatBuilder::new() 131 | .column_separator('|') 132 | .borders('|') 133 | .separators( 134 | &[format::LinePosition::Top, format::LinePosition::Bottom], 135 | format::LineSeparator::new('-', '+', '+', '+'), 136 | ) 137 | .separators( 138 | &[format::LinePosition::Title], 139 | format::LineSeparator::new('=', '+', '+', '+'), 140 | ) 141 | .padding(1, 1) 142 | .build(); 143 | table.set_format(format); 144 | table.set_titles(row!["Template Name", "Custom", "Extension"]); 145 | for table_row in rows { 146 | table.add_row(Row::new(table_row.iter().map(|s| Cell::new(s)).collect())); 147 | } 148 | table.printstd(); 149 | } 150 | 151 | fn get_templates() -> Result { 152 | let mut templates = get_built_in_templates()?; 153 | templates.append(&mut get_custom_templates()?); 154 | Ok(templates) 155 | } 156 | 157 | fn filename_extension(filename: &str) -> String { 158 | Path::new(filename) 159 | .extension() 160 | .map(|e| format!(".{}", e.to_string_lossy())) 161 | .unwrap_or_default() 162 | } 163 | 164 | fn print_templates(templates: &TemplateMap) { 165 | let table: Vec<_> = templates 166 | .iter() 167 | .map(|(k, v)| { 168 | vec![ 169 | k.clone(), 170 | match v.source { 171 | Source::BuiltIn => "", 172 | Source::Custom => "yes", 173 | } 174 | .to_string(), 175 | filename_extension(&v.filename), 176 | ] 177 | }) 178 | .collect(); 179 | 180 | print_ascii_table(&table); 181 | } 182 | 183 | fn template_not_found(name: &str, templates: &TemplateMap) -> ! { 184 | println!("Template '{}' is not found!", name); 185 | println!("Available templates in the table below:"); 186 | print_templates(templates); 187 | std::process::exit(1); 188 | } 189 | 190 | pub fn command_new(name: Option) -> Result<()> { 191 | let templates = get_templates()?; 192 | 193 | if let Some(name) = name { 194 | if let Some(template) = templates.get(&name) { 195 | println!("{}", template.contents); 196 | } else { 197 | template_not_found(&name, &templates); 198 | } 199 | } else { 200 | println!("Usage:\n$ scriptisto new