on master help sign in
Add SQLite (#27)

Wrap SQLite CLI and leverage it for interactive features. We use a patch to suppress unwanted SQLite CLI output.

commit3fb14f2a46b628317d7dbb1c67d9cf078ee272c1 authorSidney Kochman <sidney@kochman.org> dateFri 28 Jun 2019, 14:03 (7 years ago) parent25582f2
13 files changed+260−9
Makefile+1 README.md+10−9 languages/sqlite/compile+6 languages/sqlite/main.go+123 languages/sqlite/patch/patch.c+87 scripts/docker-install.sh+1 test_files/hello.sql+1 tests/sqlite/1_plus_1.exp+7 tests/sqlite/eval.exp+5 tests/sqlite/exec.exp+4 tests/sqlite/header.exp+4 tests/sqlite/quiet.exp+6 tests/sqlite/read_file.exp+5
Makefile+1
@@ -5,6 +5,7 @@ all: $(addprefix prybar-,$(filter-out R,$(shell ls languages))) ## Build all Pry
55# mtimes are updated every time we build.
66prybar-%: utils/*.go languages/%/* ## Build the Prybar binary for LANG
77 @echo "build prybar-$(*)"
8+ @if [ -f "languages/$(*)/compile" ]; then languages/$(*)/compile; fi
89 @scripts/inject.sh $(*)
910 @go generate languages/$(*)/main.go
1011 @go build -o prybar-$(*) ./languages/$(*)
README.md+10−9
@@ -5,13 +5,13 @@ REPL, different languages.
55
66## Why
77
8−At [Repl.it](https://repl.it) we're in the business of running REPLs.
8+At [Repl.it](https://repl.it), we're in the business of running REPLs.
99As it happens to be, every language implements them differently. We
1010wanted them to all behave the same: run code and drop into a REPL!
1111
1212## How it works
1313
14−Prybar, written in [Golang](https://golang.org/), maintains a common
14+Prybar, written in [Go](https://golang.org/), maintains a common
1515command-line interface that calls into a select language backend. When
1616possible, the language backends are implemented using cgo and the
1717language's C-bindings. Otherwise, they make use of a small script
@@ -36,17 +36,18 @@ written in the host language which starts a Prybar-compatible REPL.
3636
3737| language | eval | eval expression | eval file | repl | repl like eval | set prompt |
3838| ------------------------- | ---- | --------------- | --------- | ---- | -------------- | ---------- |
39+| Emacs Lisp | ✔ | ✔ | ✔ | ✔ | ✗ | ✔ |
40+| Javascript (nodejs) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
41+| Javascript (spidermonkey) | ✔ | ✗ | ✗ | ✗ | ✗ | - |
42+| Julia | ✔ | ✗ | ✔ | ✔ | ✗ | ✔ |
43+| Lua 5.1 | ✔ | ✗ | ✔ | ✔ | ✗ | ✔ |
44+| OCaml | ✔ | ✔ | ✔ | ✔ | ✗ | ✔ |
3945| Python 2.7 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
4046| Python 3.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
47+| R | ✔ | ✗ | ✗ | ✔ | ✗ | ✗ |
4148| Ruby 2.5 | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ |
42−| Lua 5.1 | ✔ | ✗ | ✔ | ✔ | ✗ | ✔ |
49+| SQLite | ✔ | ✔ | ✔ | ✔ | ✗ | ✔ |
4350| Tcl | ✔ | ✔ | ✔ | ✗ | ✗ | - |
44−| R | ✔ | ✗ | ✗ | ✔ | ✗ | ✗ |
45−| Javascript (spidermonkey) | ✔ | ✗ | ✗ | ✗ | ✗ | - |
46−| Javascript (nodejs) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
47−| Julia | ✔ | ✗ | ✔ | ✔ | ✗ | ✔ |
48−| OCaml | ✔ | ✔ | ✔ | ✔ | ✗ | ✔ |
49−| Emacs Lisp | ✔ | ✔ | ✔ | ✔ | ✗ | ✔ |
5051
5152## Build and run
5253
languages/sqlite/compilenew file+6
@@ -0,0 +1,6 @@
1+#!/bin/bash
2+
3+COMPILE_DIR=$(dirname "$0")
4+DEST_DIR=$COMPILE_DIR/../../prybar_assets/sqlite
5+mkdir $DEST_DIR
6+gcc -Wall -shared -fPIC -ldl -o $DEST_DIR/patch.so $COMPILE_DIR/patch/patch.c
languages/sqlite/main.gonew file+123
@@ -0,0 +1,123 @@
1+// Take a Prybar config and exec the SQLite CLI with it.
2+//
3+// Note: the prompt config (ps1 and ps2) must be SQL strings, i.e. single quotation
4+// marks must be properly escaped.
5+package main
6+
7+import (
8+ "fmt"
9+ "io/ioutil"
10+ "os"
11+ "os/exec"
12+ "path/filepath"
13+ "syscall"
14+
15+ "github.com/replit/prybar/utils"
16+)
17+
18+var Instance = &SQLite{}
19+
20+type SQLite struct{}
21+
22+// writeConfig writes the provided slice of strings out to temporary file and
23+// returns its pathname.
24+func writeConfig(lines []string) string {
25+ f, err := ioutil.TempFile("", "sqlite-config")
26+ if err != nil {
27+ panic(err)
28+ }
29+ defer f.Close()
30+
31+ for _, line := range lines {
32+ f.WriteString(line)
33+ }
34+
35+ return f.Name()
36+}
37+
38+// preloadQuietLib adds our LD_PRELOAD lib to environment and enables it
39+func preloadQuietLib(env []string) []string {
40+ execPath, err := os.Executable()
41+ if err != nil {
42+ panic(err)
43+ }
44+ runDir := filepath.Dir(execPath)
45+ libPath := filepath.Join(runDir, "prybar_assets", "sqlite", "patch.so")
46+ return append(env, []string{"LD_PRELOAD=" + libPath, "PRYBAR_QUIET=1"}...)
47+}
48+
49+func eval(sqlite string, config *utils.Config) {
50+ args := []string{"sqlite3"}
51+ env := os.Environ()
52+
53+ if len(config.Code) > 0 {
54+ // We have to execute provided code without showing output.
55+ // So, let's have sqlite run a command that will output to /dev/null.
56+ confLines := []string{".output /dev/null\n"}
57+ args = append(args, []string{"-init", writeConfig(confLines), ":memory:", config.Code}...)
58+
59+ // add LD_PRELOAD lib to environment to suppress initialization output
60+ env = preloadQuietLib(env)
61+ } else {
62+ // we can just run the code without a config file since we want its output
63+ args = append(args, []string{":memory:", config.Exp}...)
64+ }
65+
66+ err := syscall.Exec(sqlite, args, env)
67+ if err != nil {
68+ panic(err)
69+ }
70+}
71+
72+func interactive(sqlite string, config *utils.Config) {
73+ // main and continuation prompts
74+ confLines := []string{
75+ fmt.Sprintf(".prompt '%s' '%s'\n", config.Ps1, config.Ps2),
76+ }
77+
78+ // execute file, if specified
79+ if len(config.Args) == 1 {
80+ fileToRun := config.Args[0]
81+ confLines = append(confLines, fmt.Sprintf(".read %s\n", fileToRun))
82+ }
83+
84+ args := []string{"sqlite3", "-init", writeConfig(confLines)}
85+
86+ env := os.Environ()
87+ if config.Quiet {
88+ env = preloadQuietLib(env)
89+ }
90+
91+ err := syscall.Exec(sqlite, args, env)
92+ if err != nil {
93+ panic(err)
94+ }
95+}
96+
97+func Execute(config *utils.Config) {
98+ // sanity check
99+ if len(config.Args) > 1 {
100+ fmt.Fprint(os.Stderr, "too many arguments\n")
101+ os.Exit(1)
102+ }
103+
104+ if config.OurInteractive {
105+ fmt.Fprint(os.Stderr, "not supported\n")
106+ os.Exit(1)
107+ }
108+
109+ // make sure we have the sqlite CLI
110+ sqlite, err := exec.LookPath("sqlite3")
111+ if err != nil {
112+ panic(err)
113+ }
114+
115+ if len(config.Code) > 0 || len(config.Exp) > 0 {
116+ // we're executing provided code
117+ eval(sqlite, config)
118+ } else {
119+ // running sqlite CLI interactively
120+ interactive(sqlite, config)
121+ }
122+
123+}
languages/sqlite/patch/patch.cnew file+87
@@ -0,0 +1,87 @@
1+#include <stdarg.h>
2+#include <stdio.h>
3+#include <stdlib.h>
4+#include <string.h>
5+
6+/* a shared library designed to be LD_PRELOADed into SQLite CLI
7+
8+If PRYBAR_QUIET is set in the environment, then this suppresses the "Loading
9+resources" message due to the CLI's `-init` flag, and also hides the version
10+header and help message.
11+
12+What happens in SQLite CLI:
13+
14+1. fprintf to stderr containing the "Loading resources..." message
15+2. printf containing version and help string
16+
17+We need to suppress the output if the PRYBAR_QUIET env var is set. So, we wait
18+for the CLI to make calls to libc print functions and intercept them. If they're
19+what we expect, then we don't print anything and then advance the state as
20+necessary.
21+
22+Check out the SQLite CLI code to see what it does:
23+https://www.sqlite.org/src/artifact/4e1bcf8c70b8fb97
24+
25+*/
26+
27+// this macro lets us roll up the varargs once instead of in four separate
28+// places below.
29+#define VAR_PRINT(out, format) \
30+ { \
31+ va_list arg; \
32+ va_start(arg, format); \
33+ int rc = vfprintf(out, format, arg); \
34+ va_end(arg); \
35+ return rc; \
36+ }
37+
38+/* three states:
39+ - no output suppressed
40+ - suppressed "Loading resources..." message on stderr
41+ - suppressed version and help string on stdout
42+*/
43+static enum {
44+ INITIAL,
45+ LOADING_SUPPRESSED,
46+ VERSION_SUPPRESSED
47+} current_state = INITIAL;
48+
49+int __fprintf_chk(FILE *out, int flag, const char *format, ...) {
50+ // don't do anything; pass through print
51+ if (current_state != INITIAL) {
52+ VAR_PRINT(out, format);
53+ }
54+
55+ // check if this is the first time we're seeing the output that we're aiming
56+ // to suppress
57+ const char *expected = "-- Loading resources from %s\n";
58+ if (getenv("PRYBAR_QUIET") && out == stderr &&
59+ strcmp(format, expected) == 0) {
60+ // advance to next state and suppress output
61+ current_state = LOADING_SUPPRESSED;
62+ return 0;
63+ }
64+
65+ // this isn't the output we were looking for...
66+ VAR_PRINT(out, format);
67+}
68+
69+int __printf_chk(int flag, const char *format, ...) {
70+ // don't do anything; pass through print
71+ if (current_state != LOADING_SUPPRESSED) {
72+ VAR_PRINT(stdout, format);
73+ }
74+
75+ // check if this is the first time we're seeing the output that we're aiming
76+ // to suppress
77+ const char *expected = "SQLite version %s %.19s\nEnter \".help\" for usage "
78+ "hints.\n";
79+ if (getenv("PRYBAR_QUIET") && strcmp(format, expected) == 0) {
80+ // advance to next state and suppress output
81+ current_state = VERSION_SUPPRESSED;
82+ return 0;
83+ }
84+
85+ // this isn't the output we were looking for...
86+ VAR_PRINT(stdout, format);
87+}
scripts/docker-install.sh+1
@@ -28,6 +28,7 @@ opam
2828python-dev
2929python3-dev
3030ruby2.5-dev
31+sqlite3
3132tcl-dev
3233wget
3334
test_files/hello.sqlnew file+1
@@ -0,0 +1 @@
1+select 'hey there';
tests/sqlite/1_plus_1.expnew file+7
@@ -0,0 +1,7 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -q -i
4+expect -- "^--> "
5+send "select 1+1;\r"
6+expect -exact "2"
7+expect -exact "--> "
tests/sqlite/eval.expnew file+5
@@ -0,0 +1,5 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -e "select 1+1;"
4+expect ^2
5+expect eof
tests/sqlite/exec.expnew file+4
@@ -0,0 +1,4 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -c "select 1+1;"
4+expect ^eof
tests/sqlite/header.expnew file+4
@@ -0,0 +1,4 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -i
4+expect -gl "-- Loading resources from /tmp/sqlite-config*\r\nSQLite version 3.22.0*\r\nEnter \".help\" for usage hints.\r\n--> "
tests/sqlite/quiet.expnew file+6
@@ -0,0 +1,6 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -q -i
4+expect "^--> "
5+send ".quit \r"
6+expect eof
tests/sqlite/read_file.expnew file+5
@@ -0,0 +1,5 @@
1+#!/usr/bin/expect -f
2+
3+spawn ./prybar-sqlite -q -i ./test_files/hello.sql
4+expect -- "^hey there\r
5+--> "