├── .dockerignore
├── .editorconfig
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Readme.md
├── docs
├── api.md
├── authentication.md
├── caching.md
├── config.md
├── install.md
├── namespaced-imports.md
└── relative-imports.md
├── import.sh
└── test
├── nginx.conf
├── start-server.sh
├── static
├── foo.sh
├── foo@1.0.0.sh
├── pkg as foo.sh
├── relative-dest.sh
├── relative.sh
├── subdir
│ └── relative.sh
└── sum.rb
└── test.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !*.sh
3 | !test
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | tab_width = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | build:
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Build the Docker image
14 | run: docker build . --file Dockerfile --tag test:$(date +%s)
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /cache
2 | /?.sh
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7 as base
2 | RUN apk add --no-cache curl bash dash loksh mksh tree zsh build-base nginx
3 |
4 | # Compile `oksh` from source
5 | RUN cd /tmp && \
6 | curl -LfsS https://github.com/ibara/oksh/archive/main.tar.gz | tar xzvf - && \
7 | cd oksh* && \
8 | ./configure && make && make install && \
9 | cd .. && \
10 | rm -rf oksh*
11 |
12 | WORKDIR /usr/src
13 | RUN mkdir -p test/logs /run/nginx
14 | COPY . .
15 |
16 | FROM base
17 | RUN sh ./test/test.sh
18 |
19 | FROM base
20 | # Really `loksh`
21 | RUN ksh ./test/test.sh
22 |
23 | FROM base
24 | RUN oksh ./test/test.sh
25 |
26 | FROM base
27 | RUN mksh ./test/test.sh
28 |
29 | FROM base
30 | RUN zsh ./test/test.sh
31 |
32 | FROM base
33 | RUN dash ./test/test.sh
34 |
35 | FROM base
36 | RUN bash ./test/test.sh
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2020 the Import authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # import
2 |
3 | `import` is a simple and fast module system for Bash and other Unix shells.
4 |
5 | Inspired by Go's import command, you specify the URL of the shell script,
6 | and the `import` function downloads the file and caches it locally, _forever_.
7 |
8 | The code will never change from below your feet, and will continue to work
9 | offline.
10 |
11 |
12 | ## 👋 Example
13 |
14 | https://git.io/fAWiz ← This URL contains a simple `add` shell function:
15 |
16 | ```bash
17 | add() {
18 | expr "$1" + "$2"
19 | }
20 | ```
21 |
22 | You can use the `import` function to download, cache, and use that function in
23 | your own script:
24 |
25 | ```bash
26 | #!/usr/bin/env import
27 |
28 | # The URL is downloaded once, cached forever, and then sourced
29 | import "https://git.io/fAWiz"
30 |
31 | add 20 22
32 | # 42
33 | ```
34 |
35 |
36 | ## ⚙️ Compatibility
37 |
38 | The core `import` function is fully POSIX-compliant, and maximum compatibility
39 | is the goal. `import` is unit tested against the following shell implementations:
40 |
41 | * [ash](https://en.wikipedia.org/wiki/Almquist_shell) - Almquist Shell (BusyBox `ash` and Debian `dash`)
42 | * [ksh](https://en.wikipedia.org/wiki/KornShell) - KornShell (`oksh`, `mksh` and `loksh` flavors)
43 | * [zsh](https://en.wikipedia.org/wiki/Z_shell) - Z Shell
44 | * [bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) - GNU's Bourne Again Shell
45 |
46 |
47 | ## 📚 Documentation
48 |
49 | * [Authentication](./docs/authentication.md) - Making private GitHub repos work
50 | * [API](./docs/api.md) - Application programming interface reference
51 | * [Caching](./docs/caching.md) - Explanation of the caching strategy
52 | * [Configuration](./docs/config.md) - Customizing `import` with environment variables
53 | * [Installation](./docs/install.md) - Installing and bootstrapping `import`
54 | * [Namespaced Imports](./docs/namespaced-imports.md) - Top-level and community imports
55 | * [Relative Imports](./docs/relative-imports.md) - Implementation for relative imports
56 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | ## 📜 API
2 |
3 | ### `import "$url"`
4 |
5 | The core `import` function downloads the `$url` parameter and
6 | [caches](./caching.md) it to the file system. Finally, it sources
7 | the downloaded script.
8 |
9 | ```bash
10 | #!/usr/bin/env import
11 | import "https://import.sh/string@0.2.0"
12 |
13 | echo InPuT | string_upper
14 | # INPUT
15 | ```
16 |
17 |
18 | ### `import_file "$url"`
19 |
20 | Uses the same download and caching infrastructure as `import`, but prints the
21 | local file path instead of sourcing the file. This enables working with arbitrary
22 | files such as scripts from other languages, simple data files, binary files, etc.
23 |
24 | ```bash
25 | #!/usr/bin/env import
26 |
27 | ruby "$(import_file https://import.sh/importpw/import/test/static/sum.rb)" 9 10 11 12
28 | # 42
29 | ```
30 |
31 |
32 | ### `import_cache_dir "$name"`
33 |
34 | Returns the operating system specific path to the cache directory for the given
35 | `$name`. This function honors the [XDG Base Directory
36 | Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
37 | by utilizing the `$XDG_CACHE_HOME` environment variable, if defined. Otherwise it
38 | falls back to using:
39 |
40 | * `$HOME/Library/Caches` on macOS
41 | * `$LOCALAPPDATA` on Windows
42 | * `$HOME/.cache` everywhere else
43 |
44 | ```bash
45 | #!/usr/bin/env import
46 |
47 | import_cache_dir example
48 | # /Users/nate/Library/Caches/example
49 |
50 | XDG_CACHE_HOME=/cache import_cache_dir example
51 | # /cache/example
52 | ```
53 |
54 |
55 | ### `import_cache_dir_import`
56 |
57 | Returns the operating system specific path to the cache directory that files
58 | imported using `import` are written to. This function returns the contents the
59 | `$IMPORT_CACHE` environment variable, if defined. Otherwise it returns the result
60 | of `import_cache_dir import.sh`.
61 |
62 | ```bash
63 | #!/usr/bin/env import
64 |
65 | import_cache_dir_import
66 | # /Users/nate/Library/Caches/import.sh
67 |
68 | IMPORT_CACHE=/tmp import_cache_dir_import
69 | # /tmp
70 | ```
71 |
--------------------------------------------------------------------------------
/docs/authentication.md:
--------------------------------------------------------------------------------
1 | ## 🔑 Authentication
2 |
3 | Because `import` uses `curl`, you can use the standard [`.netrc` file
4 | format](https://ec.haxx.se/usingcurl-netrc.html) to define your username
5 | and passwords to the server you are importing from.
6 |
7 | For exampe, to make script files in private GitHub repos accessible, create a
8 | `~/.netrc` file that contains something like:
9 |
10 | ```ini
11 | machine raw.githubusercontent.com
12 | login 231a4a02aeb1fbcf164f7c444ae5a211c1451d95
13 | password x-oauth-basic
14 | ```
15 |
16 | The `login` token is a [GitHub "personal access token"](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/).
17 | Follow the instructions in that link to create one for yourself.
18 |
19 | After that, an `import` call to a private repo will work as expected:
20 |
21 | ```bash
22 | import "my-organization/private-repo@1.0.0"
23 | ```
24 |
25 | Your GitHub credentials **ARE NEVER** given to the `import.sh` server.
26 | They are only used _locally_ by `curl` once the server redirects to the
27 | private repo URL.
28 |
--------------------------------------------------------------------------------
/docs/caching.md:
--------------------------------------------------------------------------------
1 | ## 💸 Caching
2 |
3 | Caching is a core concept in `import`. Scripts are downloaded _exactly once_, and
4 | then cached on your filesystem _forever_ (unless the `IMPORT_RELOAD=1` environment
5 | variable is set).
6 |
7 | ```bash
8 | #!/usr/bin/env import
9 |
10 | # Import script files to the `/tmp` directory
11 | IMPORT_CACHE="/tmp"
12 |
13 | # Log information related to `import` to stderr
14 | IMPORT_DEBUG=1
15 |
16 | # Force a fresh download of script files (like Shift + Reload in the browser)
17 | IMPORT_RELOAD=1
18 |
19 | import assert
20 | ```
21 |
22 | If you run this example, then you can see the file structure and order of
23 | operations because of the debug logging:
24 |
25 | ```
26 | import: importing 'assert'
27 | import: normalized URL 'https://import.sh/assert'
28 | import: HTTP GET https://import.sh/assert
29 | import: resolved location 'https://import.sh/assert' -> 'https://raw.githubusercontent.com/importpw/assert/master/assert.sh'
30 | import: calculated hash 'https://import.sh/assert' -> '0a1c5188c768b3b150f1a8a104bb71a3fa160aad'
31 | import: creating symlink ‘/tmp/links/https/import.sh/assert’ -> ‘../../../data/0a1c5188c768b3b150f1a8a104bb71a3fa160aad’
32 | import: successfully downloaded 'https://import.sh/assert' -> '/tmp/data/0a1c5188c768b3b150f1a8a104bb71a3fa160aad'
33 | import: sourcing '/tmp/links/https/import.sh/assert'
34 | ```
35 |
36 | Now let's take a look at what the actual directory structure looks like:
37 |
38 | ```
39 | $ tree /tmp
40 | /tmp
41 | ├── data
42 | │ └── bf671d3752778f91ad0884ff81b3e963af9e4a4f
43 | ├── links
44 | │ └── https
45 | │ └── import.sh
46 | │ └── assert -> ../../../data/bf671d3752778f91ad0884ff81b3e963af9e4a4f
47 | └── locations
48 | └── https
49 | └── import.sh
50 | └── assert
51 | ```
52 |
53 | `import` generates **three** subdirectories under the `IMPORT_CACHE` directory:
54 |
55 | * `data` - The raw shell scripts, named after the sha1sum of the file contents
56 | * `links` - Symbolic links that are named according to the import URL
57 | * `locations` - Files named according to the import URL that point to the _real_ URL
58 |
59 | ### ⚙️ Cache Location
60 |
61 | If the `IMPORT_CACHE` environment variable is not set, the cache location
62 | defaults to the directory `import.sh` in the OS-specific user cache directory.
63 | For this user cache directory `import` considers (in order):
64 |
65 | * `$XDG_CACHE_HOME` ([usually](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) set on Linux)
66 | * `$LOCALAPPDATA` (usually set on Windows)
67 | * `$HOME/Library/Caches` on macOS and `$HOME/.cache` everywhere else
68 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | ## 🔨 Configuration
2 |
3 | `import` is configurable via the following environment variables:
4 |
5 | | Name | Description |
6 | |:---------------:|----------------------------------------------------------------------------------------------------|
7 | | `IMPORT_CACHE` | The directory where imported files will be cached.
Defaults to `~/.import-cache`. |
8 | | `IMPORT_CURL_OPTS` | Additional options to pass to `curl`. See the [`curl` manpage](https://curl.haxx.se/docs/manpage.html) for its docs. |
9 | | `IMPORT_DEBUG` | If this variable is set, then debugging information related to `import` will be printed to stderr. |
10 | | `IMPORT_RELOAD` | If this variable is set, then all `import` calls will force-reload from the source URL. |
11 | | `IMPORT_TRACE` | Path to a filename to print imported URLs for tracing purposes. |
12 | | `IMPORT_SERVER` | The server to use for [namespaced imports](./namespaced-imports.md).
Defaults to `https://import.sh`. |
13 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | ## 🔽 Installation
2 |
3 | `import` is a single, self-contained shell script. Installation is as simple
4 | as downloading the script into your `$PATH` and giving it executable permissions.
5 | Alternatively, it can be downloaded automatically within a script that uses `import`.
6 |
7 | ### 👢 Bootstrapping the `import` function
8 |
9 | The installation can be anywhere on the `$PATH`. For example, to install `import` to
10 | `/usr/local/bin`, run the following:
11 |
12 | ```bash
13 | curl -sfLS https://import.sh > /usr/local/bin/import
14 | chmod +x /usr/local/bin/import
15 | ```
16 |
17 | Once you have the `import` script installed, there are two preferred ways to
18 | utilize it in your shell scripts: _shebang_ or _source_.
19 |
20 |
21 | #### Shebang
22 |
23 | The most straightforward way to specify `import` as the entry point of the script
24 | using the "shebang" feature of executable files:
25 |
26 | ```bash
27 | #!/usr/bin/env import
28 |
29 | type import
30 | ```
31 |
32 | Note that this method will use the interpreter located at `/bin/sh`, which usually
33 | implies baseline POSIX features. If you need more control over which interpreter
34 | is used then see the next method.
35 |
36 | #### Source
37 |
38 | Another way to bootstrap `import` is to simply source it into your script.
39 | This method gives you control over which interpreter is used. For example,
40 | if you need bash-specific features, you can specify to use it in the shebang,
41 | and then source the `import` script:
42 |
43 | ```bash
44 | #!/bin/bash
45 |
46 | . "$(command -v import)"
47 |
48 | type import
49 | ```
50 |
51 | ### 🦿 Automatic download
52 |
53 | An alternative approach is to automatically download `import` in your shell
54 | script itself without requiring manual installation.
55 |
56 | #### Eval
57 |
58 | It is possible to `curl` + `eval` the import function directly into your shell
59 | script.
60 |
61 | ```bash
62 | #!/bin/sh
63 |
64 | eval "$(curl -sfLS https://import.sh)"
65 |
66 | type import
67 | ```
68 |
69 | Note that this method is not as ideal as the shebang/sourcing methods, because
70 | this version incurs an HTTP request to retrieve the import function every time
71 | the script is run, and it won't work offline.
72 |
73 | #### Download & Cache
74 |
75 | Finally, it is possible to download and cache the `import` script itself by
76 | using the following snippet. This combines the convenience of the eval approach
77 | without the cost of an HTTP request on each run, but requires a slightly unwieldy
78 | bit of code in each shell script that uses `import`.
79 |
80 | ```bash
81 | #!/bin/sh
82 |
83 | [ "$(uname -s)" = "Darwin" ] && __i="$HOME/Library/Caches" || __i="$HOME/.cache" && __i="${IMPORT_CACHE:-${XDG_CACHE_HOME:-${LOCALAPPDATA:-${__i}}}/import.sh}/import" && [ -r "$__i" ] || curl -sfLSo "$__i" --create-dirs https://import.sh && . "$__i" && unset __i
84 |
85 | type import
86 | ```
87 |
88 | Explanation: the complexity lies almost completely in finding out the default
89 | cache location on different operating systems in sync with the `import` script
90 | as detailed in the [caching](caching.md) documentation. Following that, the
91 | snippet checks if the `import` script exists in the cache, downloads and stores
92 | it via `curl` if is missing, and finally sources it.
93 |
--------------------------------------------------------------------------------
/docs/namespaced-imports.md:
--------------------------------------------------------------------------------
1 | ## Namespaced Imports
2 |
3 | Any `import` where the beginning portion (up to the first slash) of the
4 | URL _does not contain a `.`_ is considered a **namespaced import**.
5 |
6 | A namespaced import means that the `IMPORT_SERVER` (which defaults to
7 | https://import.sh) is prepended to the import URL. For example, these
8 | two import invocations are identical:
9 |
10 | * `import "assert"`
11 | * `import "https://import.sh/assert"`
12 |
13 |
14 | ## Example
15 |
16 | Let's take a look at importing this [tootallnate/hello][hello] "Hello World"
17 | import from GitHub:
18 |
19 | ```bash
20 | #!/usr/bin/env import
21 | import "tootallnate/hello"
22 |
23 | hello
24 | # Hello, from @TooTallNate!
25 | ```
26 |
27 |
28 | ## The `import.sh` server
29 |
30 | The default `IMPORT_SERVER` is https://import.sh, which serves GitHub
31 | repositories that are _"import-compatible"_, according to the following
32 | conventions:
33 |
34 | * The main import syntax is `import /`
35 | * The entry point of the import is the file with the name of the repo with a `.sh` suffix
36 | * If there is no `/` in the import path, then the default org ([importpw][]) is applied
37 | * Specific tags may be referenced by appending an `@` to the end
38 |
39 |
40 | ## Top-level imports
41 |
42 | The [importpw][] GitHub organization houses the top-level namespace imports.
43 | A top-level import happens when there is no `/` in the import path.
44 |
45 | For example, the `assert` module includes functions that write simple unit
46 | testing scripts:
47 |
48 | ```bash
49 | #!/usr/bin/env import
50 | import "assert"
51 |
52 | assert 1 = 2
53 | # assertion failed: 1 = 2
54 | ```
55 |
56 | Here are some useful top-level imports:
57 |
58 | * [array](https://import.sh/array)
59 | * [assert](https://import.sh/assert)
60 | * [confirm](https://import.sh/confirm)
61 | * [dns](https://import.sh/dns)
62 | * [emitter](https://import.sh/emitter)
63 | * [http](https://import.sh/http)
64 | * [os](https://import.sh/os)
65 | * [path](https://import.sh/path)
66 | * [prompt](https://import.sh/prompt)
67 | * [querystring](https://import.sh/querystring)
68 | * [string](https://import.sh/string)
69 | * [tcp](https://import.sh/tcp)
70 |
71 | See the [importpw][] org on GitHub for the complete listing of repositories.
72 |
73 |
74 | ## Community imports
75 |
76 | Here are some GitHub repositories that are known to be compatible with `import`:
77 |
78 | * [kward/log4sh](https://import.sh/kward/log4sh)
79 | * [kward/shflags](https://import.sh/kward/shflags)
80 | * [kward/shunit2](https://import.sh/kward/shunit2)
81 | * [robwhitby/shakedown](https://import.sh/robwhitby/shakedown)
82 | * [tootallnate/nexec](https://import.sh/tootallnate/nexec)
83 |
84 | (Send a [pull request](https://github.com/importpw/import/pulls) if you would like to have an import listed here)
85 |
86 | [hello]: https://github.com/TooTallNate/hello
87 | [importpw]: https://github.com/importpw
88 |
--------------------------------------------------------------------------------
/docs/relative-imports.md:
--------------------------------------------------------------------------------
1 | ## Relative Imports
2 |
3 | Any `import` that begins with `./` or `../` is considered a **relative import**.
4 |
5 | Relative imports reference a file located _relative_ to the file that is importing
6 | it. This provides a mechanism for modularization (breaking up the logic into
7 | multiple files) and code forking (for example, importing different implementations
8 | of a function based on the shell interpreter).
9 |
10 |
11 | ## Implementation Details
12 |
13 | Relative imports are made possible primarily because of the `Location` and/or
14 | `Content-Location` HTTP headers provided by the server that provides the
15 | imported URL.
16 |
17 | When a script is imported, the HTTP headers are parsed, and the _final_
18 | `Location`/`Content-Location` header is considered the "location" of the script.
19 | This final URL gets cached to the filesystem in the `locations` directory.
20 |
21 | ### Example
22 |
23 | Perhaps an example will help illustrate. If you inspect the response headers for
24 | the [`tootallnate/hello`](https://import.sh/tootallnate/hello), then you can see
25 | the `content-location` header is present:
26 |
27 | ```
28 | #!/bin/sh
29 |
30 | curl -sI https://import.sh/tootallnate/hello | grep -i location
31 | # content-location: https://raw.githubusercontent.com/tootallnate/hello/master/hello.sh
32 | ```
33 |
34 | `import` keeps tracks of these URL locations, so that from _within the `hello.sh`
35 | script_, any relative import, let's say `import ./foo.sh`, will be normalized to
36 | relative of the current URL location.
37 |
--------------------------------------------------------------------------------
/import.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | import_log() {
4 | echo "import:" "$@" >&2
5 | }
6 |
7 | import_debug() {
8 | [ "${IMPORT_DEBUG-}" = "1" ] && import_log "$@" || true
9 | }
10 |
11 | # Only `shasum` is present on MacOS by default,
12 | # but only `sha1sum` is present on Alpine by default
13 | __import_shasum="$(command -v sha1sum)" || __import_shasum="$(command -v shasum)" || {
14 | r=$?
15 | import_log "No \`shasum\` or \`sha1sum\` command present"
16 | exit "$r"
17 | }
18 | import_debug "Using '$__import_shasum'"
19 |
20 | # Empty tracing file if it already exists, when the env var is set
21 | [ -n "${IMPORT_TRACE-}" ] && :>| "$IMPORT_TRACE"
22 |
23 | import_usage() {
24 | echo "Usage: import \"org/repo/mod.sh\"" >&2
25 | echo "" >&2
26 | echo " Documentation: https://import.sh" >&2
27 | echo " Core Modules: https://github.com/importpw" >&2
28 | echo "" >&2
29 | echo " Examples:" >&2
30 | echo " import \"assert\" # import the latest commit of the 'assert' module " >&2
31 | echo " import \"assert@2.1.3\" # import the tag \`2.1.3\` of the 'assert' module" >&2
32 | echo " import \"tootallnate/hello\" # import from the GitHub repo \`tootallnate/hello\`" >&2
33 | echo " import \"https://git.io/fAWiz\" # import from a fully qualified URL" >&2
34 | return 2
35 | }
36 |
37 | import_parse_location() {
38 | local location="$1"
39 | local headers="$2"
40 | local location_header=""
41 |
42 | # Print `x-import-warning` headers
43 | grep -i '^x-import-warning:' < "$headers" | while IFS='' read -r line; do
44 | echo "import: warning - $(echo "$line" | awk -F": " '{print $2}' | tr -d \\r)" >&2
45 | done
46 |
47 | # Find the final `Location` or `Content-Location` header
48 | location_header="$(grep -i '^location\|^content-location:' < "$headers" | tail -n1)"
49 | if [ -n "$location_header" ]; then
50 | location="$(echo "$location_header" | awk -F": " '{print $2}' | tr -d \\r)"
51 | fi
52 | echo "$location"
53 | }
54 |
55 | # The base directory for the import cache.
56 | # Defaults to `import.sh` in the user cache directory specified by `$XDG_CACHE_HOME`
57 | # or `$LOCALAPPDATA` (falling back to `$HOME/Library/Caches` on macOS and
58 | # `$HOME/.cache` everywhere else).
59 | # May be configured by setting the `IMPORT_CACHE` variable.
60 | # On AWS Lambda, `$HOME` is not defined but `~` works.
61 | # Furthermore, make sure we can always set IMPORT_CACHE even if HOME is undefined.
62 | import_cache_dir() {
63 | local home="${HOME:-"$(echo ~)"}"
64 | local ucd_fallback="$home/.cache"
65 | [ "$(uname -s)" = "Darwin" ] && ucd_fallback="$home/Library/Caches"
66 | echo "${XDG_CACHE_HOME:-${LOCALAPPDATA:-$ucd_fallback}}/$1"
67 | }
68 |
69 | import_cache_dir_import() {
70 | echo "${IMPORT_CACHE:-$(import_cache_dir import.sh)}"
71 | }
72 |
73 | import() {
74 | local url="$*"
75 | local url_path=""
76 |
77 | if [ -z "$url" ]; then
78 | import_usage
79 | return
80 | fi
81 |
82 | import_debug "Importing '$url'"
83 |
84 | # If this is a relative import than it need to be based off of
85 | # the parent import's resolved URL location.
86 | case "$url" in
87 | (./*) url="$(dirname "$__import_location")/$url";;
88 | (../*) url="$(dirname "$__import_location")/$url";;
89 | esac
90 |
91 | local cache=""
92 | cache="$(import_cache_dir_import)"
93 |
94 | # Apply the default server if the user is doing an implicit import
95 | if ! echo "$url" | grep "://" > /dev/null && ! echo "$url" | awk -F/ '{print $1}' | awk -F@ '{print $1}' | grep '\.' > /dev/null; then
96 | url="${IMPORT_SERVER-https://import.sh}/$url"
97 | import_debug "Normalized URL '$url'"
98 | fi
99 |
100 | # Print the URL to the tracing file if the env var is set
101 | [ -n "${IMPORT_TRACE-}" ] && echo "$url" >> "$IMPORT_TRACE"
102 |
103 | url_path="$(echo "$url" | sed 's/\:\///')"
104 | local cache_path="$cache/links/$url_path"
105 |
106 | if [ ! -e "$cache_path" ] || [ "${IMPORT_RELOAD-}" = "1" ]; then
107 | # Ensure that the directory containing the symlink for this import exists.
108 | local dir
109 | dir="$(dirname "$url_path")"
110 |
111 | local link_dir="$cache/links/$dir"
112 | mkdir -p "$link_dir" "$cache/data" "$cache/locations/$dir" >&2 || return
113 |
114 | # Resolve the cache and link dirs with `pwd` now that the directories exist.
115 | cache="$( ( cd "$cache" && pwd ) )" || return
116 | link_dir="$( ( cd "$link_dir" && pwd ) )" || return
117 | cache_path="$cache/links/$url_path"
118 |
119 | # Download the requested file to a temporary place so that the shasum
120 | # can be computed to determine the proper final filename.
121 | local location=""
122 | local tmpfile="$cache_path.tmp"
123 | local tmpheader="$cache_path.header"
124 | local locfile="$cache/locations/$url_path"
125 | local qs="?"
126 | if echo "$url" | grep '?' > /dev/null; then
127 | qs="&"
128 | fi
129 | import_log "Downloading $url"
130 | local url_with_qs="${url}${qs}format=raw"
131 | import_retry curl -sfLS \
132 | --netrc-optional \
133 | --dump-header "$tmpheader" \
134 | ${IMPORT_CURL_OPTS-} \
135 | "$url_with_qs" > "$tmpfile" || {
136 | local r=$?
137 | import_log "Failed to download: $url_with_qs" >&2
138 | rm -f "$tmpfile" "$tmpheader" || true
139 | return "$r"
140 | }
141 |
142 | # Now that the HTTP request has been resolved, parse the "Location"
143 | location="$(import_parse_location "$url" "$tmpheader")" || return
144 | import_debug "Resolved location '$url' -> '$location'"
145 | echo "$location" > "$locfile"
146 | rm -f "$tmpheader"
147 |
148 | # Calculate the sha1 hash of the contents of the downloaded file.
149 | local hash
150 | hash="$("$__import_shasum" < "$tmpfile" | { read -r first rest; echo "$first"; })" || return
151 | import_debug "Calculated hash '$url' -> '$hash'"
152 |
153 | local hash_file="$cache/data/$hash"
154 |
155 | # If the hashed file doesn't exist then move it into place,
156 | # otherwise delete the temp file - it's no longer needed.
157 | if [ -f "$hash_file" ]; then
158 | rm -f "$tmpfile" || return
159 | else
160 | mv "$tmpfile" "$hash_file" || return
161 | fi
162 |
163 | # Create a relative symlink for this import pointing to the hashed file.
164 | local relative
165 | local cache_start
166 | cache_start="$(expr "${#cache}" + 1)" || return
167 | relative="$(echo "$link_dir" | awk '{print substr($0,'$cache_start')}' | sed 's/\/[^/]*/..\//g')data/$hash" || return
168 | [ -n "${IMPORT_DEBUG-}" ] && printf "import: Creating symlink " >&2
169 | ln -fs${IMPORT_DEBUG:+v} "$relative" "$cache_path" >&2 || return
170 |
171 | import_debug "Successfully downloaded '$url' -> '$hash_file'"
172 | else
173 | import_debug "Already cached '$url'"
174 | fi
175 |
176 | # Reset the `import` command args. There's not really a good reason to pass
177 | # the URL to the sourced script, and in fact could cause undesirable results.
178 | # i.e. This is required to make `import.sh/kward/shunit2` work out of the box.
179 | set --
180 |
181 | # At this point, the file has been saved to the cache so
182 | # either source it or print it.
183 | if [ -z "${print-}" ]; then
184 | import_debug "Sourcing '$cache_path'"
185 | local __import_parent_location="${__import_location-}"
186 | __import_location="$(cat "$cache/locations/$url_path")" || return
187 | . "$cache_path" || return
188 | __import_location="$__import_parent_location"
189 | else
190 | import_debug "Printing '$cache_path'"
191 | echo "$cache_path"
192 | fi
193 | }
194 |
195 | import_file() {
196 | print=1 import "$@"
197 | }
198 |
199 | import_retry() {
200 | local exit_code=""
201 | local retry_count="0"
202 | local number_of_retries="${retries:-5}"
203 |
204 | while [ "$retry_count" -lt "$number_of_retries" ]; do
205 | exit_code="0"
206 | "$@" || exit_code=$?
207 | if [ "$exit_code" -eq 0 ]; then
208 | break
209 | fi
210 | # TODO: add exponential backoff
211 | sleep 1
212 | retry_count=$(( retry_count + 1 ))
213 | done
214 |
215 | return "$exit_code"
216 | }
217 |
218 | # For `#!/usr/bin/env import`
219 | if [ -n "${ZSH_EVAL_CONTEXT-}" ]; then
220 | if [ "${ZSH_EVAL_CONTEXT-}" = "toplevel" ]; then
221 | __import_entrypoint="1"
222 | fi
223 | elif [ "$(echo "$0" | cut -c1)" != "-" ] && [ "$(basename "$0" .sh)" = "import" ]; then
224 | __import_entrypoint="1"
225 | fi
226 |
227 | if [ -n "${__import_entrypoint-}" ]; then
228 | # Parse argv
229 | while [ $# -gt 0 ]; do
230 | case "$1" in
231 | -s=*|--shell=*) __import_shell="${1#*=}"; shift 1;;
232 | -s|--shell) __import_shell="$2"; shift 2;;
233 | -c) __import_command="$2"; shift 2;;
234 | -*) echo "import: unknown option $1" >&2 && exit 2;;
235 | *) break;;
236 | esac
237 | done
238 |
239 | if [ -n "${__import_shell-}" ]; then
240 | # If the script requested a specific shell, then relaunch using it
241 | exec "$__import_shell" "$0" "$@"
242 | elif [ -n "${__import_command-}" ]; then
243 | eval "$__import_command"
244 | else
245 | __import_entrypoint="$1"
246 | shift
247 | . "$__import_entrypoint"
248 | fi
249 | fi
250 |
--------------------------------------------------------------------------------
/test/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes 1;
2 |
3 | events {
4 | worker_connections 1024;
5 | }
6 |
7 | http {
8 | server {
9 | listen 12006 default_server;
10 | listen [::]:12006 default_server;
11 |
12 | root ./static;
13 |
14 | location /warning {
15 | add_header "X-Import-Warning" "This server has moved to xxxxx.sh";
16 | return 302 /foo;
17 | }
18 |
19 | location / {
20 | try_files $uri $uri.sh $uri/ =404;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/start-server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd test
3 | exec nginx \
4 | -p "$PWD/" \
5 | -c "$PWD/nginx.conf" \
6 | -g 'daemon off;'
7 |
--------------------------------------------------------------------------------
/test/static/foo.sh:
--------------------------------------------------------------------------------
1 | foo() {
2 | echo foo
3 | }
4 |
--------------------------------------------------------------------------------
/test/static/foo@1.0.0.sh:
--------------------------------------------------------------------------------
1 | foo1() {
2 | echo foo1
3 | }
4 |
--------------------------------------------------------------------------------
/test/static/pkg as foo.sh:
--------------------------------------------------------------------------------
1 | foo() {
2 | echo this is foo
3 | }
4 |
--------------------------------------------------------------------------------
/test/static/relative-dest.sh:
--------------------------------------------------------------------------------
1 | relative() {
2 | echo relative
3 | }
4 |
--------------------------------------------------------------------------------
/test/static/relative.sh:
--------------------------------------------------------------------------------
1 | import ./relative-dest.sh
2 | import ./subdir/relative.sh
3 |
--------------------------------------------------------------------------------
/test/static/subdir/relative.sh:
--------------------------------------------------------------------------------
1 | subdir_rel() {
2 | echo subdir_rel
3 | }
4 |
--------------------------------------------------------------------------------
/test/static/sum.rb:
--------------------------------------------------------------------------------
1 | puts ARGV.map(&:to_i).sum
2 |
--------------------------------------------------------------------------------
/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | ./test/start-server.sh &
5 | nginx_pid="$!"
6 | nginx_addr="http://127.0.0.1:12006"
7 | echo "nginx pid $nginx_pid"
8 |
9 | # Time for nginx to boot up
10 | sleep 1
11 |
12 | finish() {
13 | echo "Killing nginx (pid $nginx_pid)"
14 | kill "$nginx_pid"
15 | exit
16 | }
17 | trap finish EXIT INT QUIT
18 |
19 | IMPORT_CACHE="$PWD/cache"
20 | IMPORT_DEBUG=1
21 | IMPORT_RELOAD=1
22 | IMPORT_SERVER="${nginx_addr}"
23 | . "./import.sh"
24 |
25 | # Test basic `foo` import
26 | import foo
27 | test "$(foo)" = "foo"
28 |
29 | # Test basic import with `@` symbol
30 | import foo@1.0.0
31 | test "$(foo1)" = "foo1"
32 |
33 | # Test 404
34 | r=0
35 | import does_not_exist || r="$?"
36 | test "$r" -ne 0
37 |
38 | # Test "X-Import-Warning"
39 | if ! import warning 2>&1 | grep "This server has moved to xxxxx.sh" >/dev/null; then
40 | echo "X-Import-Warning was not rendered" >&2
41 | exit 1
42 | fi
43 |
44 | # Test relative import
45 | import relative
46 | test "$(relative)" = "relative"
47 | test "$(subdir_rel)" = "subdir_rel"
48 |
49 | # Test multiple words
50 | import pkg as foo
51 | test "$(foo)" = "this is foo"
52 |
53 | # Test import_file
54 | sum_rb_path="$(import_file "$nginx_addr/sum.rb")"
55 | test "$sum_rb_path" = "$IMPORT_CACHE/links/http/127.0.0.1:12006/sum.rb"
56 | diff -q "$sum_rb_path" "test/static/sum.rb"
57 |
58 | # Test import with print=1 (equivalent to import_file; supported for backwards compatibility)
59 | sum_rb_path="$(print=1 import "$nginx_addr/sum.rb")"
60 | test "$sum_rb_path" = "$IMPORT_CACHE/links/http/127.0.0.1:12006/sum.rb"
61 | diff -q "$sum_rb_path" "test/static/sum.rb"
62 |
63 |
64 | echo "Tests passed!"
65 |
--------------------------------------------------------------------------------