├── .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 | --------------------------------------------------------------------------------