├── .github └── workflows │ ├── test.yaml │ └── vagrant.yaml ├── .gitignore ├── LICENSE ├── Makefile.in ├── README.md ├── STYLE.md ├── Vagrantfile ├── completions ├── README.md └── try.bash ├── config.guess ├── config.sub ├── configure.ac ├── docs ├── try.1.md ├── try_logo.png └── try_pip_install_example.gif ├── install-sh ├── man └── .gitignore ├── package.nix ├── scripts ├── README.md ├── check_trycase.sh ├── check_version.sh ├── lint.sh └── run_tests.sh ├── shell.nix ├── test ├── README.md ├── all-commit-cases.sh ├── command_substitution.sh ├── dev_urandom.sh ├── empty_summary.sh ├── exit_status.sh ├── explore.sh ├── fail.sh.skip ├── hidden_variables.sh ├── ignore_flag.sh ├── merge_multiple_dirs.sh ├── missing_unionfs_mergerfs.sh ├── mkdir_on_file.sh ├── network.sh ├── newline-filename.sh ├── nonexistent_sandbox.sh ├── pipeline.sh ├── resources │ └── file.txt.gz ├── reuse_problematic_sandbox.sh ├── reuse_sandbox.sh ├── stdstream.sh ├── summary.sh ├── symlinks.sh ├── tempfiles.sh ├── toplevel-perms.sh ├── touch_and_rm_D_flag_commit.sh ├── touch_and_rm_no_flag.sh ├── touch_and_rm_with_cleanup.sh.skip ├── try-commit-c.sh ├── unzip_D_flag_commit.sh ├── unzip_D_flag_commit_without_cleanup.sh └── unzip_no_flag.sh ├── try └── utils ├── .gitignore ├── README.md ├── ignores.c ├── ignores.h ├── try-commit.c ├── try-summary.c └── version.h /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '17 14 * * *' 8 | 9 | jobs: 10 | dist: 11 | strategy: 12 | fail-fast: true 13 | 14 | runs-on: ubuntu-latest 15 | if: github.event.pull_request.draft == false 16 | 17 | steps: 18 | - name: Allow unprivileged user namespaces (for Ubuntu 24.04) 19 | run: | 20 | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 21 | 22 | - name: Install dependencies 23 | run: | 24 | sudo apt-get install expect mergerfs attr pandoc 25 | 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | ref: ${{ github.event.pull_request.head.sha }} 30 | 31 | - name: Check autoscan 32 | run: | 33 | autoscan | tee autoscan.out 34 | ! [ -s autoscan.out ] 35 | 36 | - name: Configure and build manpage 37 | run: | 38 | autoconf 39 | ./configure 40 | make dist 41 | 42 | - name: Upload dist tarball 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: try-dist.tgz 46 | path: try-*.tgz 47 | 48 | test-dist: 49 | needs: dist 50 | strategy: 51 | fail-fast: false 52 | 53 | runs-on: ubuntu-latest 54 | if: github.event.pull_request.draft == false 55 | 56 | steps: 57 | - name: Allow unprivileged user namespaces (for Ubuntu 24.04) 58 | run: | 59 | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 60 | 61 | - name: Install dependencies 62 | run: | 63 | sudo apt-get install expect mergerfs attr pandoc 64 | 65 | - name: Download dist tarball 66 | uses: actions/download-artifact@v4 67 | 68 | - name: Unpack tarball; configure and build utilities 69 | run: | 70 | tar xzf try-dist.tgz/try-*.tgz --strip-components=1 71 | rm -r try-dist.tgz 72 | ./configure 73 | make all 74 | 75 | - name: Run tests with shell fallbacks 76 | run: | 77 | ! which try-summary 78 | ! which try-commit 79 | scripts/run_tests.sh 80 | 81 | - name: Install utilities 82 | run: | 83 | sudo make install 84 | 85 | - name: Run tests with utilities 86 | run: | 87 | which try-summary 88 | which try-commit 89 | scripts/run_tests.sh 90 | 91 | - name: Check manpage 92 | run: | 93 | man -w try 94 | 95 | test-checkout: 96 | strategy: 97 | fail-fast: false 98 | 99 | runs-on: ubuntu-latest 100 | if: github.event.pull_request.draft == false 101 | 102 | steps: 103 | - name: Allow unprivileged user namespaces (for Ubuntu 24.04) 104 | run: | 105 | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 106 | 107 | - name: Install dependencies 108 | run: | 109 | sudo apt-get install expect mergerfs attr pandoc 110 | 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | with: 114 | ref: ${{ github.event.pull_request.head.sha }} 115 | 116 | - name: Configure and build utilities 117 | run: | 118 | autoconf 119 | ./configure 120 | make all 121 | 122 | - name: Run tests with shell fallbacks 123 | run: | 124 | ! which try-summary 125 | ! which try-commit 126 | scripts/run_tests.sh 127 | 128 | - name: Install utilities 129 | run: | 130 | sudo make install 131 | 132 | - name: Run tests with utilities 133 | run: | 134 | which try-summary 135 | which try-commit 136 | scripts/run_tests.sh 137 | 138 | - name: Check manpage 139 | run: | 140 | man -w try 141 | 142 | lint: 143 | runs-on: ubuntu-latest 144 | 145 | steps: 146 | - name: Checkout 147 | uses: actions/checkout@v4 148 | with: 149 | ref: ${{ github.event.pull_request.head.sha }} 150 | 151 | - name: Run scripts/lint.sh 152 | run: scripts/lint.sh 153 | 154 | trycase-check: 155 | runs-on: ubuntu-latest 156 | steps: 157 | - uses: actions/checkout@v4 158 | with: 159 | ref: ${{ github.event.pull_request.head.sha }} 160 | 161 | - name: Run check_trycase.sh 162 | run: scripts/check_trycase.sh 163 | 164 | version-check: 165 | runs-on: ubuntu-latest 166 | if: github.event.pull_request.draft == false 167 | 168 | steps: 169 | - name: Checkout 170 | uses: actions/checkout@v4 171 | with: 172 | ref: ${{ github.event.pull_request.head.sha }} 173 | 174 | - name: Check version consistency (script, manpage, include) 175 | run: scripts/check_version.sh 176 | 177 | shellcheck: 178 | runs-on: ubuntu-latest 179 | steps: 180 | - name: Checkout 181 | uses: actions/checkout@v4 182 | with: 183 | ref: ${{ github.event.pull_request.head.sha }} 184 | 185 | - name: Run ShellCheck 186 | uses: ludeeus/action-shellcheck@master 187 | with: 188 | ignore_paths: >- 189 | completions 190 | ./configure 191 | ./install-sh 192 | 193 | nix: 194 | runs-on: ubuntu-latest 195 | steps: 196 | - name: Checkout 197 | uses: actions/checkout@v4 198 | with: 199 | ref: ${{ github.event.pull_request.head.sha }} 200 | 201 | - name: Allow unprivileged user namespaces (for Ubuntu 24.04) 202 | run: | 203 | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 204 | 205 | - name: Install Nix 206 | uses: DeterminateSystems/nix-installer-action@main 207 | 208 | - name: Run tests 209 | run: | 210 | nix-shell --run "sh scripts/run_tests.sh" 211 | 212 | 213 | prerelease: 214 | needs: 215 | - test-checkout 216 | - test-dist 217 | - version-check 218 | - lint 219 | - shellcheck 220 | - trycase-check 221 | runs-on: ubuntu-latest 222 | if: ${{ github.ref == 'refs/heads/main' }} 223 | 224 | steps: 225 | - name: Download binaries 226 | uses: actions/download-artifact@v4 227 | 228 | - name: Rename tarball 229 | run: | 230 | mv try-dist.tgz/try-*.tgz try-latest.tgz 231 | rmdir try-dist.tgz 232 | 233 | - name: Deploy 'latest' release 234 | uses: "marvinpinto/action-automatic-releases@latest" 235 | with: 236 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 237 | automatic_release_tag: "latest" 238 | prerelease: true 239 | title: "Latest distribution tarball" 240 | files: | 241 | try-latest.tgz 242 | -------------------------------------------------------------------------------- /.github/workflows/vagrant.yaml: -------------------------------------------------------------------------------- 1 | name: Distro and LVM tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: '17 20 * * *' 10 | 11 | jobs: 12 | vagrant: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | vagrant_target: 17 | - debian 18 | - debianrustup 19 | - debianlvm 20 | - rocky9 21 | - fedora39 22 | runs-on: self-hosted 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Cleanup 28 | run: bash /home/runner/cleanup.sh 29 | 30 | - name: Start vagrant box 31 | run: vagrant up ${{ matrix.vagrant_target }} 32 | 33 | - name: Stop vagrant box 34 | run: vagrant destroy -f ${{ matrix.vagrant_target }} 35 | if: ${{ success() || failure() }} # we want to run this step even if the last step failed 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/bash_workspace/ 2 | test/results/ 3 | test/try_workspace/ 4 | *~ 5 | .vagrant/ 6 | .vagrant 7 | autom4te.cache/ 8 | config.log 9 | config.status 10 | Makefile 11 | configure.scan 12 | autoscan.log 13 | configure 14 | try-0.2.0.tgz 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The PaSh 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 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | prefix=@prefix@ 2 | exec_prefix=@exec_prefix@ 3 | bindir=@bindir@ 4 | datarootdir=@datarootdir@ 5 | mandir=@mandir@ 6 | 7 | CC=@CC@ 8 | CFLAGS=@CFLAGS@ 9 | CPPFLAGS=@CPPFLAGS@ 10 | INSTALL=@INSTALL@ 11 | 12 | .PHONY: all install test dist lint clean 13 | 14 | DISTDIR=@PACKAGE_TARNAME@-@PACKAGE_VERSION@ 15 | DISTTGZ=$(DISTDIR).tgz 16 | 17 | TARGETS=$(if $(findstring yes,@enable_utils@),utils/try-summary utils/try-commit) man/try.1.gz 18 | 19 | all: $(TARGETS) 20 | 21 | install: $(TARGETS) 22 | $(INSTALL) -d $(bindir) 23 | $(INSTALL) -m 755 try $(bindir) 24 | ifeq (@enable_utils@, yes) 25 | $(INSTALL) -m 755 utils/try-summary $(bindir) 26 | $(INSTALL) -m 755 utils/try-commit $(bindir) 27 | endif 28 | $(INSTALL) -d $(mandir)/man1 29 | $(INSTALL) -m 644 man/try.1.gz $(mandir)/man1 30 | 31 | dist: $(DISTTGZ) 32 | 33 | $(DISTTGZ): $(DISTDIR) 34 | tar czf $@ $< 35 | rm -rf $(DISTDIR) 36 | 37 | $(DISTDIR): man/try.1 man/try.1.gz 38 | mkdir $@ 39 | cp configure configure.ac config.guess config.sub install-sh LICENSE Makefile.in README.md STYLE.md try Vagrantfile $@ 40 | cp -R completions $@ 41 | cp -R docs $@ 42 | cp -R man $@ 43 | cp -R scripts $@ 44 | cp -R test $@ 45 | cp -R utils $@ 46 | 47 | man/try.1.gz: man/try.1 48 | cat $< | gzip >$@ 49 | 50 | man/try.1: docs/try.1.md 51 | pandoc --standalone --from markdown-smart --to man $< -o $@ 52 | 53 | utils/try-summary: utils/ignores.o utils/try-summary.o 54 | $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -g $^ 55 | 56 | utils/try-commit: utils/ignores.o utils/try-commit.o 57 | $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -g $^ 58 | 59 | test: try utils/try-summary utils/try-commit 60 | scripts/run_tests.sh 61 | 62 | lint: 63 | scripts/lint.sh 64 | 65 | clean: 66 | ifeq (@enable_manpage@, yes) 67 | -rm man/try.1 man/try.1.gz 68 | endif 69 | -rm utils/*.o 70 | -rm utils/try-summary utils/try-commit 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # try 2 | 3 | try logo 4 | 5 | "Do, or do not. There is no try." 6 | 7 | We're setting out to change that: `try cmd` and commit---or not. 8 | 9 | ## Description 10 | [![Main workflow](https://github.com/binpash/try/actions/workflows/test.yaml/badge.svg)](https://github.com/binpash/try/actions/workflows/test.yaml) 11 | [![License](https://img.shields.io/badge/License-MIT-blue)](#license) 12 | [![issues - try](https://img.shields.io/github/issues/binpash/try)](https://github.com/binpash/try/issues) 13 | 14 | `try` lets you run a command and inspect its effects before changing your live system. `try` uses Linux's [namespaces (via `unshare`)](https://docs.kernel.org/userspace-api/unshare.html) and the [overlayfs](https://docs.kernel.org/filesystems/overlayfs.html) union filesystem. 15 | 16 | Please note that `try` is a prototype and not a full sandbox, and should not be used to execute 17 | commands that you don't already trust on your system, (i.e. network calls are all allowed) 18 | 19 | try gif 20 | 21 | ## Getting Started 22 | 23 | ### Dependencies 24 | 25 | `try` relies on the following Debian packages 26 | 27 | * `attr` (for `getfattr`) 28 | * `pandoc` and `autoconf` (if working from a GitHub clone) 29 | 30 | In cases where overlayfs doesn't work on nested mounts, you will need either 31 | [mergerfs](https://github.com/trapexit/mergerfs) or [unionfs](https://github.com/rpodgorny/unionfs-fuse). `try` should be able to autodetect them, but you can specify the path to mergerfs or unionfs with -U (e.g. `try -U ~/.local/bin/unionfs`) 32 | 33 | To run `try`'s test suite (`scripts/run_tests.sh`), you will need: 34 | 35 | * `bash` 36 | * `expect` 37 | * `curl` 38 | 39 | `try` has been tested on the following distributions: 40 | 41 | * `Ubuntu 20.04 LTS` or later 42 | * `Debian 12` 43 | * `Fedora 38` 44 | * `Centos 9 Stream 5.14.0-325.el9` 45 | * `Arch 6.1.33-1-lts` 46 | * `Alpine 6.1.34-1-lts` 47 | * `Rocky 9 5.14.0-284.11.1.el9_2` 48 | * `SteamOS 3.4.8 5.13.0-valve36-1-neptune` 49 | 50 | *Note that try will only work on [Linux 51 | 5.11](https://github.com/torvalds/linux/commit/92dbc9dedccb9759c7f9f2f0ae6242396376988f) 52 | or higher for overlayfs to work in a user namespace.* 53 | 54 | ### Installing 55 | 56 | There are three ways to install try: 57 | 58 | 1. **The quick and janky way: grab the script.** You only need the [`try` script](https://raw.githubusercontent.com/binpash/try/main/try). Put it in your `PATH` and you're ready to go. You won't have documentation or utility support, but it should work as is. 59 | 60 | 2. **By cloning the repository.** Run the following: 61 | ```ShellSession 62 | $ git clone https://github.com/binpash/try.git 63 | $ autoconf && ./configure && make && sudo make install 64 | ``` 65 | You should now have a fully featured `try`, including the support utilities (which should help `try` run faster) and manpage. Run `make test` to confirm that everything works. 66 | 67 | 3. **By using a source distribution.** Download `try-XXX.tgz` from [the release page](https://github.com/binpash/try/releases). You can get the latest prerelease by downloading [`try-latest.tgz`](https://github.com/binpash/try/releases/download/latest/try-latest.tgz). You can then install similarly to the above: 68 | ```ShellSession 69 | $ git clone https://github.com/binpash/try.git 70 | $ ./configure && make && sudo make install 71 | ``` 72 | 73 | The repository and source distribution are slightly different: the repository does not include the `configure` script (generated by `autoconf`) or the manpage (generated by `pandoc`), but the source distribution does. 74 | 75 | #### Arch Linux 76 | 77 | `try` is present in [AUR](https://aur.archlinux.org/packages/try), you can install it with your preferred AUR helper: 78 | 79 | ```shellsession 80 | yay -S try 81 | ``` 82 | 83 | or manually: 84 | 85 | ```shellsession 86 | git clone https://aur.archlinux.org/try.git 87 | cd try 88 | makepkg -sic 89 | ``` 90 | 91 | #### Nix/NixOS 92 | 93 | `try` is present in [nixpkgs](https://search.nixos.org/packages?channel=unstable&show=try&from=0&size=50&sort=relevance&type=packages&query=try) and maintained by us. 94 | 95 | ```shellsession 96 | nix-shell -p try 97 | ``` 98 | 99 | ## Example Usage 100 | 101 | `try` is a higher-order command, like `xargs`, `exec`, `nohup`, or `find`. For example, to install a package via `pip3`, you can invoke `try` as follows: 102 | 103 | ```ShellSession 104 | $ try pip3 install libdash 105 | ... # output continued below 106 | ``` 107 | 108 | By default, `try` will ask you to commit the changes made at the end of its execution. 109 | 110 | ```ShellSession 111 | ... 112 | Defaulting to user installation because normal site-packages is not writeable 113 | Collecting libdash 114 | Downloading libdash-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (254 kB) 115 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 254.6/254.6 KB 2.1 MB/s eta 0:00:00 116 | Installing collected packages: libdash 117 | Successfully installed libdash-0.3.1 118 | WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv 119 | 120 | Changes detected in the following files: 121 | 122 | /tmp/tmp.zHCkY9jtIT/upperdir/home/gliargovas/.local/lib/python3.10/site-packages/libdash/ast.py (modified/added) 123 | /tmp/tmp.zHCkY9jtIT/upperdir/home/gliargovas/.local/lib/python3.10/site-packages/libdash/_dash.py (modified/added) 124 | /tmp/tmp.zHCkY9jtIT/upperdir/home/gliargovas/.local/lib/python3.10/site-packages/libdash/__init__.py (modified/added) 125 | /tmp/tmp.zHCkY9jtIT/upperdir/home/gliargovas/.local/lib/python3.10/site-packages/libdash/__pycache__/printer.cpython-310.pyc (modified/added) 126 | /tmp/tmp.zHCkY9jtIT/upperdir/home/gliargovas/.local/lib/python3.10/site-packages/libdash/__pycache__/ast.cpython-310.pyc (modified/added) 127 | 128 | 129 | Commit these changes? [y/N] y 130 | ``` 131 | 132 | Sometimes, you might want to pre-execute a command and commit its result at a later time. Running `try -n` will print the overlay directory on STDOUT without committing the result. 133 | 134 | ```ShellSession 135 | $ try -n "curl https://sh.rustup.rs | sh" 136 | /tmp/tmp.uCThKq7LBK 137 | ``` 138 | 139 | Alternatively, you can specify your own existing overlay directory using the `-D [dir]` flag: 140 | 141 | ```ShellSession 142 | $ mkdir rustup-sandbox 143 | $ try -D rustup-sandbox "curl https://sh.rustup.rs | sh" 144 | $ ls rustup-sandbox 145 | temproot upperdir workdir 146 | ``` 147 | 148 | As you can see from the output above, `try` has created an overlay environment in the `rustup-sandbox` directory. 149 | 150 | Manually inspecting upperdir reveals the changes to the files made inside the overlay during the execution of the previous command with *try*: 151 | 152 | ```ShellSession 153 | ~/try/rustup-sandbox/upperdir$ du -hs . 154 | 1.2G . 155 | ``` 156 | 157 | You can inspect the changes made inside a given overlay directory using `try`: 158 | 159 | ```ShellSession 160 | $ try summary rustup-sandbox/ | head 161 | 162 | Changes detected in the following files: 163 | 164 | rustup-sandbox//upperdir/home/ubuntu/.profile (modified/added) 165 | rustup-sandbox//upperdir/home/ubuntu/.bashrc (modified/added) 166 | rustup-sandbox//upperdir/home/ubuntu/.rustup/update-hashes/stable-x86_64-unknown-linux-gnu (modified/added) 167 | rustup-sandbox//upperdir/home/ubuntu/.rustup/settings.toml (modified/added) 168 | rustup-sandbox//upperdir/home/ubuntu/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/libstd-8389830094602f5a.so (modified/added) 169 | rustup-sandbox//upperdir/home/ubuntu/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/etc/lldb_commands (modified/added) 170 | rustup-sandbox//upperdir/home/ubuntu/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/etc/gdb_lookup.py (modified/added) 171 | ``` 172 | 173 | You can also choose to commit the overlay directory contents: 174 | 175 | ```ShellSession 176 | $ try commit rustup-sandbox 177 | ``` 178 | 179 | You can also run `try explore` to open your current shell in try, or `/try 180 | explore /tmp/tmp.X6OQb5tJwr` to explore an existing sandbox. 181 | 182 | To specify multiple lower directories for overlay (by merging them together), you can use the `-L` (implies `-n`) flag followed by a colon-separated list of directories. The directories on the left have higher precedence and can overwrite the directories on the right: 183 | 184 | ```ShellSession 185 | $ try -D /tmp/sandbox1 "echo 'File 1 Contents - sandbox1' > file1.txt" 186 | $ try -D /tmp/sandbox2 "echo 'File 2 Contents - sandbox2' > file2.txt" 187 | $ try -D /tmp/sandbox3 "echo 'File 2 Contents - sandbox3' > file2.txt" 188 | 189 | # Now use the -L flag to merge both sandbox directories together, with sandbox3 having precedence over sandbox2 190 | $ try -L "/tmp/sandbox3:/tmp/sandbox2:/tmp/sandbox1" "cat file1.txt file2.txt" 191 | File 1 Contents - sandbox1 192 | File 2 Contents - sandbox3 193 | ``` 194 | 195 | In this example, `try` will merge `/sandbox1`, `/sandbox2` and `/sandbox3` together before mounting the overlay. This way, you can combine the contents of multiple `try` sandboxes. 196 | 197 | 198 | ## Known Issues 199 | Any command that interacts with other users/groups will fail since only the 200 | current user's UID/GID are mapped. However, the [future 201 | branch](https://github.com/binpash/try/tree/future) has support for uid/mapping; 202 | please refer to the that branch's readme for installation instructions for the 203 | uid/gidmapper (root access is required for installation). 204 | 205 | Shell quoting may be unintuitive, you may expect `try bash -c "echo a"` to work, 206 | however, try will actually execute `bash -c echo a`, which will not result in 207 | `a` being printed. [We are currently not planning on resolving this 208 | behavior.](https://github.com/binpash/try/issues/155#issuecomment-2078556237) 209 | 210 | Please also report any issue you run into while using the future branch! 211 | 212 | ## Version History 213 | 214 | * v0.2.0 - 2023-07-24 215 | - Refactor tests. 216 | - Improved linting. 217 | - Hide `try`-internal variables from scripts. 218 | - Style guide. 219 | - Testing in Vagrant. 220 | - Support nested mounts. 221 | - Resolve issues with `userxattr`. 222 | - Better support for `unionfs`. 223 | - Use `/bin/sh`, not `/bin/bash`. 224 | - `-i` flag to ignore paths. 225 | - Interactive improvements. 226 | 227 | * v0.1.0 - 2023-06-25 228 | - Initial release. 229 | 230 | ## See Also 231 | 232 | [checkinstall](http://checkinstall.izto.org/) (unmaintained) 233 | 234 | checkinstall keeps track of all the files created or modified by your 235 | installation script, builds a standard binary package and installs it in your 236 | system. This package can then be easily installed, managed, and removed using 237 | the package manager of your Linux distribution. It helps in maintaining a clean 238 | and organized system by keeping track of installed software and its 239 | dependencies. 240 | 241 | ## License 242 | 243 | This project is licensed under the MIT License - see LICENSE for details. 244 | 245 | Copyright (c) 2023 The PaSh Authors. 246 | -------------------------------------------------------------------------------- /STYLE.md: -------------------------------------------------------------------------------- 1 | The [POSIX 2 | shell](https://pubs.opengroup.org/onlinepubs/9699919799/idx/shell.html) 3 | is a tricky programming language. Here are the style guidelines for 4 | `try`. 5 | 6 | # Indentation and whitespace 7 | 8 | Indent four spaces. Do not use tabs. Blank lines should have no 9 | whitespace; there should be no trailing whitespace on lines. Try to 10 | avoid line continuations. 11 | 12 | Align the commands of a case: 13 | 14 | ```sh 15 | case "$1" in 16 | (summary) : ${SANDBOX_DIR=$2} 17 | summary;; 18 | (commit) : ${SANDBOX_DIR=$2} 19 | commit;; 20 | (explore) : ${SANDBOX_DIR=$2} 21 | try "$SHELL";; 22 | (--) shift 23 | try "$@";; 24 | (*) try "$@";; 25 | esac 26 | ``` 27 | 28 | # Write POSIX code 29 | 30 | Do not use: 31 | 32 | - `<(command substitutions)` 33 | - `(( static arithmetic expression ))` 34 | - `[[ static test expressions ]]` 35 | - `array=(assignment)` or `${array[access]}` 36 | - the `function` keyword 37 | - `brace{expansion,}` 38 | - `${param:offset}`, `${!matching}`, `${pattern/substi/tution}` 39 | - `source` (it's called `.`) 40 | 41 | Do not use `LOCAL_ASSIGNMENT=foo f` when `f` could be a function. 42 | 43 | Write: 44 | 45 | ```sh 46 | VAR_NAME=value 47 | export VAR_NAME 48 | ``` 49 | 50 | # Quoting 51 | 52 | It is better to be safe than sorry. 53 | 54 | # Variables 55 | 56 | Use alphabetical names with underscores for word separations. 57 | 58 | Variables which live past their scope should be `CAPITALIZED_LIKE_THIS`. Local 59 | variables should be `lowercase_like_this`. The `local` keyword is unspecified in 60 | POSIX and should not be used. 61 | 62 | Do not use braces for variable references: every POSIX shell will 63 | correctly parse `"$SANDBOX_DIR/workdir"` with `SANDBOX_DIR` as the 64 | variable name. You only need braces when sticking on valid variable 65 | name characters, like in `${SANDBOX_DIR}2`. 66 | 67 | # Command substitutions 68 | 69 | Always use `$(...)`; never use backticks. 70 | 71 | # Function definitions 72 | 73 | Function names are `in_lowercase_please`. Put a function header above 74 | function definitions. (Exception: generated scripts can be terser, but 75 | should still include important comments.) Always use braces in a 76 | function definition, starting on the same line after a space. Unless 77 | you are using `$@`, rename positional parameters. Avoid obscure 78 | calling conventions: either use `$@` or name your parameters. 79 | 80 | ```sh 81 | ################################################################################ 82 | # Summarize an overlay 83 | ################################################################################ 84 | 85 | summary() { 86 | ... 87 | } 88 | ``` 89 | 90 | # Commands 91 | 92 | Redirections go at the end of commands. There should be no spaces 93 | anywhere in a redirect: write `2>/dev/null`, not `2> /dev/null`. 94 | 95 | Prefer `echo` when possible (but only use POSIX flags). 96 | 97 | Call it `[`, not `test`. Prefer `&&` and `||` over `-a` and `-o` when 98 | using `[`, but definitely use them for `find`. 99 | 100 | Use `[ "..." ]` to test whether `...` is empty or not; there is no 101 | reason to use `-n`. Do not forget quotes, though! 102 | 103 | When using `case`, put the `;;` on the last command (not its own 104 | line). Do not omit the `;;` on the last case. Use both parentheses for 105 | each case, even though the left-hand one is optional. 106 | 107 | ## Semicolons 108 | 109 | Avoid semicolons in control flow. Write: 110 | 111 | ```sh 112 | if [ ... ] 113 | then 114 | ... 115 | fi 116 | ``` 117 | 118 | The shell is terse enough without any effort on your part. Don't write 119 | `c1; c2`; write: 120 | 121 | ```sh 122 | c1 123 | c2 124 | ``` 125 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | 3 | config.vm.provider "virtualbox" do |vb| 4 | vb.memory = 8192 5 | vb.cpus = 2 6 | end 7 | 8 | # Regular debian testing box 9 | config.vm.define "debian" do |debian| 10 | debian.vm.box = "generic/debian12" 11 | debian.vm.provision "file", source: "./", destination: "/home/vagrant/try" 12 | debian.vm.provision "shell", privileged: false, inline: " 13 | sudo apt-get update 14 | sudo apt-get install -y git expect curl attr pandoc gcc make autoconf mergerfs 15 | sudo chown -R vagrant:vagrant try 16 | cd try 17 | scripts/run_tests.sh 18 | 19 | autoconf && ./configure && make 20 | sudo make install 21 | which try-commit || exit 2 22 | 23 | scripts/run_tests.sh 24 | " 25 | end 26 | 27 | # Regular debian testing box but we try the rustup oneliner 28 | config.vm.define "debianrustup" do |debianrustup| 29 | debianrustup.vm.box = "generic/debian12" 30 | debianrustup.vm.provision "file", source: "./", destination: "/home/vagrant/try" 31 | debianrustup.vm.provision "shell", privileged: false, inline: " 32 | sudo apt-get update 33 | sudo apt-get install -y curl attr pandoc gcc make autoconf mergerfs 34 | sudo chown -R vagrant:vagrant try 35 | cd try 36 | mkdir rustup 37 | ./try -D rustup \"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\" 38 | ls -lah rustup/upperdir/home/vagrant/.cargo/bin 39 | 40 | rm -rf rustup 41 | autoconf && ./configure && make 42 | sudo make install 43 | which try-commit || exit 2 44 | 45 | mkdir rustup 46 | ./try -D rustup \"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\" 47 | ls -lah rustup/upperdir/home/vagrant/.cargo/bin 48 | " 49 | end 50 | 51 | # Regular debian testing box with LVM 52 | config.vm.define "debianlvm" do |debianlvm| 53 | debianlvm.vm.box = "generic/debian12" 54 | debianlvm.vm.provision "file", source: "./", destination: "/home/vagrant/try" 55 | debianlvm.vm.provision "shell", privileged: false, inline: " 56 | sudo apt-get update 57 | sudo apt-get install -y git expect lvm2 mergerfs curl attr pandoc gcc make autoconf mergerfs 58 | 59 | # Create an image for the lvm disk 60 | sudo fallocate -l 2G /root/lvm_disk.img 61 | 62 | # Setup a loopback device 63 | sudo losetup /dev/loop0 /root/lvm_disk.img 64 | 65 | # Create the lv physicalvolume, volumegroup, and logicalvolumes 66 | sudo pvcreate /dev/loop0 67 | sudo vgcreate vg0 /dev/loop0 68 | sudo lvcreate -n lv0 -l 50%FREE vg0 69 | sudo lvcreate -n lv1 -l 100%FREE vg0 70 | 71 | sudo mkfs.ext4 /dev/vg0/lv0 72 | sudo mkfs.ext4 /dev/vg0/lv1 73 | sudo mkdir /mnt/lv0 74 | sudo mount /dev/vg0/lv0 /mnt/lv0 75 | sudo mkdir /mnt/lv0/lv1 76 | sudo mount /dev/vg0/lv1 /mnt/lv0/lv1 77 | 78 | # This is intentional, if we moved try to lv1 it'd work since itself does not contain a nested mount 79 | sudo mv /home/vagrant/try /mnt/lv0 80 | sudo chown -R vagrant:vagrant /mnt/lv0/try 81 | 82 | cd /mnt/lv0/try 83 | scripts/run_tests.sh 84 | 85 | autoconf && ./configure && make 86 | sudo make install 87 | which try-commit || exit 2 88 | 89 | scripts/run_tests.sh 90 | " 91 | end 92 | 93 | # Regular rocky testing box 94 | config.vm.define "rocky9" do |rocky| 95 | rocky.vm.box = "generic/rocky9" 96 | rocky.vm.provision "file", source: "./", destination: "/home/vagrant/try" 97 | rocky.vm.provision "shell", privileged: false, inline: " 98 | sudo yum install -y git expect curl attr pandoc fuse 99 | wget https://github.com/trapexit/mergerfs/releases/download/2.40.2/mergerfs-2.40.2-1.el9.x86_64.rpm 100 | sudo rpm -i mergerfs-2.40.2-1.el9.x86_64.rpm 101 | sudo chown -R vagrant:vagrant try 102 | cd try 103 | TRY_TOP=$(pwd) scripts/run_tests.sh 104 | autoconf && ./configure && make 105 | sudo make install 106 | which try-commit || exit 2 107 | TRY_TOP=$(pwd) scripts/run_tests.sh 108 | " 109 | end 110 | # 111 | # Regular rocky testing box 112 | config.vm.define "fedora39" do |fedora| 113 | fedora.vm.box = "generic/fedora39" 114 | fedora.vm.provision "file", source: "./", destination: "/home/vagrant/try" 115 | fedora.vm.provision "shell", privileged: false, inline: " 116 | sudo yum install -y git expect curl attr pandoc fuse 117 | wget https://github.com/trapexit/mergerfs/releases/download/2.40.2/mergerfs-2.40.2-1.fc39.x86_64.rpm 118 | sudo rpm -i mergerfs-2.40.2-1.fc39.x86_64.rpm 119 | sudo chown -R vagrant:vagrant try 120 | cd try 121 | TRY_TOP=$(pwd) scripts/run_tests.sh 122 | autoconf && ./configure && make 123 | sudo make install 124 | which try-commit || exit 2 125 | TRY_TOP=$(pwd) scripts/run_tests.sh 126 | " 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /completions/README.md: -------------------------------------------------------------------------------- 1 | # Shell completions 2 | 3 | This directory holds completions for the `bash` shell. 4 | 5 | You can apply completions locally by running: 6 | 7 | ```Bash 8 | source try.bash 9 | ``` 10 | 11 | Alternatively, to enable *try* shell completion by default, consider adding the following line to your `.bashrc`: 12 | ```Bash 13 | source /completions/try.bash 14 | ``` 15 | 16 | To enable *try* shell completion for all users, 17 | consider copying the bash completion script to the `/etc/bash_completion.d/` directory: 18 | ```Bash 19 | sudo cp try.bash /etc/bash_completion.d/ 20 | ``` 21 | -------------------------------------------------------------------------------- /completions/try.bash: -------------------------------------------------------------------------------- 1 | _try() { 2 | local i cur prev opts cmds 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | cmd="" 7 | opts="" 8 | 9 | for i in ${COMP_WORDS[@]} 10 | do 11 | case "${i}" in 12 | (try) cmd="try";; 13 | 14 | (*) ;; 15 | esac 16 | done 17 | 18 | case "${cmd}" in 19 | (try) 20 | opts="-n -y -v -h -x -i -D -U -L summary commit explore" 21 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] 22 | then 23 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 24 | return 0 25 | fi 26 | case "${prev}" in 27 | (-L) 28 | COMPREPLY=($(compgen -d "${cur}")) 29 | return 0;; 30 | (-D) 31 | COMPREPLY=($(compgen -d "${cur}")) 32 | return 0;; 33 | (-U) 34 | COMPREPLY=($(compgen -c "${cur}")) 35 | return 0;; 36 | (commit) 37 | COMPREPLY=($(compgen -d "${cur}")) 38 | return 0;; 39 | (summary) 40 | COMPREPLY=($(compgen -d "${cur}")) 41 | return 0;; 42 | (*) 43 | COMPREPLY=();; 44 | esac 45 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 46 | return 0;; 47 | esac 48 | } 49 | 50 | complete -F _try -o bashdefault -o default try 51 | -------------------------------------------------------------------------------- /config.sub: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Configuration validation subroutine script. 3 | # Copyright 1992-2022 Free Software Foundation, Inc. 4 | 5 | # shellcheck disable=SC2006,SC2268 # see below for rationale 6 | 7 | timestamp='2022-01-03' 8 | 9 | # This file is free software; you can redistribute it and/or modify it 10 | # under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, see . 21 | # 22 | # As a special exception to the GNU General Public License, if you 23 | # distribute this file as part of a program that contains a 24 | # configuration script generated by Autoconf, you may include it under 25 | # the same distribution terms that you use for the rest of that 26 | # program. This Exception is an additional permission under section 7 27 | # of the GNU General Public License, version 3 ("GPLv3"). 28 | 29 | 30 | # Please send patches to . 31 | # 32 | # Configuration subroutine to validate and canonicalize a configuration type. 33 | # Supply the specified configuration type as an argument. 34 | # If it is invalid, we print an error message on stderr and exit with code 1. 35 | # Otherwise, we print the canonical config type on stdout and succeed. 36 | 37 | # You can get the latest version of this script from: 38 | # https://git.savannah.gnu.org/cgit/config.git/plain/config.sub 39 | 40 | # This file is supposed to be the same for all GNU packages 41 | # and recognize all the CPU types, system types and aliases 42 | # that are meaningful with *any* GNU software. 43 | # Each package is responsible for reporting which valid configurations 44 | # it does not support. The user should be able to distinguish 45 | # a failure to support a valid configuration from a meaningless 46 | # configuration. 47 | 48 | # The goal of this file is to map all the various variations of a given 49 | # machine specification into a single specification in the form: 50 | # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM 51 | # or in some cases, the newer four-part form: 52 | # CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM 53 | # It is wrong to echo any other type of specification. 54 | 55 | # The "shellcheck disable" line above the timestamp inhibits complaints 56 | # about features and limitations of the classic Bourne shell that were 57 | # superseded or lifted in POSIX. However, this script identifies a wide 58 | # variety of pre-POSIX systems that do not have POSIX shells at all, and 59 | # even some reasonably current systems (Solaris 10 as case-in-point) still 60 | # have a pre-POSIX /bin/sh. 61 | 62 | me=`echo "$0" | sed -e 's,.*/,,'` 63 | 64 | usage="\ 65 | Usage: $0 [OPTION] CPU-MFR-OPSYS or ALIAS 66 | 67 | Canonicalize a configuration name. 68 | 69 | Options: 70 | -h, --help print this help, then exit 71 | -t, --time-stamp print date of last modification, then exit 72 | -v, --version print version number, then exit 73 | 74 | Report bugs and patches to ." 75 | 76 | version="\ 77 | GNU config.sub ($timestamp) 78 | 79 | Copyright 1992-2022 Free Software Foundation, Inc. 80 | 81 | This is free software; see the source for copying conditions. There is NO 82 | warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." 83 | 84 | help=" 85 | Try \`$me --help' for more information." 86 | 87 | # Parse command line 88 | while test $# -gt 0 ; do 89 | case $1 in 90 | --time-stamp | --time* | -t ) 91 | echo "$timestamp" ; exit ;; 92 | --version | -v ) 93 | echo "$version" ; exit ;; 94 | --help | --h* | -h ) 95 | echo "$usage"; exit ;; 96 | -- ) # Stop option processing 97 | shift; break ;; 98 | - ) # Use stdin as input. 99 | break ;; 100 | -* ) 101 | echo "$me: invalid option $1$help" >&2 102 | exit 1 ;; 103 | 104 | *local*) 105 | # First pass through any local machine types. 106 | echo "$1" 107 | exit ;; 108 | 109 | * ) 110 | break ;; 111 | esac 112 | done 113 | 114 | case $# in 115 | 0) echo "$me: missing argument$help" >&2 116 | exit 1;; 117 | 1) ;; 118 | *) echo "$me: too many arguments$help" >&2 119 | exit 1;; 120 | esac 121 | 122 | # Split fields of configuration type 123 | # shellcheck disable=SC2162 124 | saved_IFS=$IFS 125 | IFS="-" read field1 field2 field3 field4 <&2 134 | exit 1 135 | ;; 136 | *-*-*-*) 137 | basic_machine=$field1-$field2 138 | basic_os=$field3-$field4 139 | ;; 140 | *-*-*) 141 | # Ambiguous whether COMPANY is present, or skipped and KERNEL-OS is two 142 | # parts 143 | maybe_os=$field2-$field3 144 | case $maybe_os in 145 | nto-qnx* | linux-* | uclinux-uclibc* \ 146 | | uclinux-gnu* | kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* \ 147 | | netbsd*-eabi* | kopensolaris*-gnu* | cloudabi*-eabi* \ 148 | | storm-chaos* | os2-emx* | rtmk-nova*) 149 | basic_machine=$field1 150 | basic_os=$maybe_os 151 | ;; 152 | android-linux) 153 | basic_machine=$field1-unknown 154 | basic_os=linux-android 155 | ;; 156 | *) 157 | basic_machine=$field1-$field2 158 | basic_os=$field3 159 | ;; 160 | esac 161 | ;; 162 | *-*) 163 | # A lone config we happen to match not fitting any pattern 164 | case $field1-$field2 in 165 | decstation-3100) 166 | basic_machine=mips-dec 167 | basic_os= 168 | ;; 169 | *-*) 170 | # Second component is usually, but not always the OS 171 | case $field2 in 172 | # Prevent following clause from handling this valid os 173 | sun*os*) 174 | basic_machine=$field1 175 | basic_os=$field2 176 | ;; 177 | zephyr*) 178 | basic_machine=$field1-unknown 179 | basic_os=$field2 180 | ;; 181 | # Manufacturers 182 | dec* | mips* | sequent* | encore* | pc533* | sgi* | sony* \ 183 | | att* | 7300* | 3300* | delta* | motorola* | sun[234]* \ 184 | | unicom* | ibm* | next | hp | isi* | apollo | altos* \ 185 | | convergent* | ncr* | news | 32* | 3600* | 3100* \ 186 | | hitachi* | c[123]* | convex* | sun | crds | omron* | dg \ 187 | | ultra | tti* | harris | dolphin | highlevel | gould \ 188 | | cbm | ns | masscomp | apple | axis | knuth | cray \ 189 | | microblaze* | sim | cisco \ 190 | | oki | wec | wrs | winbond) 191 | basic_machine=$field1-$field2 192 | basic_os= 193 | ;; 194 | *) 195 | basic_machine=$field1 196 | basic_os=$field2 197 | ;; 198 | esac 199 | ;; 200 | esac 201 | ;; 202 | *) 203 | # Convert single-component short-hands not valid as part of 204 | # multi-component configurations. 205 | case $field1 in 206 | 386bsd) 207 | basic_machine=i386-pc 208 | basic_os=bsd 209 | ;; 210 | a29khif) 211 | basic_machine=a29k-amd 212 | basic_os=udi 213 | ;; 214 | adobe68k) 215 | basic_machine=m68010-adobe 216 | basic_os=scout 217 | ;; 218 | alliant) 219 | basic_machine=fx80-alliant 220 | basic_os= 221 | ;; 222 | altos | altos3068) 223 | basic_machine=m68k-altos 224 | basic_os= 225 | ;; 226 | am29k) 227 | basic_machine=a29k-none 228 | basic_os=bsd 229 | ;; 230 | amdahl) 231 | basic_machine=580-amdahl 232 | basic_os=sysv 233 | ;; 234 | amiga) 235 | basic_machine=m68k-unknown 236 | basic_os= 237 | ;; 238 | amigaos | amigados) 239 | basic_machine=m68k-unknown 240 | basic_os=amigaos 241 | ;; 242 | amigaunix | amix) 243 | basic_machine=m68k-unknown 244 | basic_os=sysv4 245 | ;; 246 | apollo68) 247 | basic_machine=m68k-apollo 248 | basic_os=sysv 249 | ;; 250 | apollo68bsd) 251 | basic_machine=m68k-apollo 252 | basic_os=bsd 253 | ;; 254 | aros) 255 | basic_machine=i386-pc 256 | basic_os=aros 257 | ;; 258 | aux) 259 | basic_machine=m68k-apple 260 | basic_os=aux 261 | ;; 262 | balance) 263 | basic_machine=ns32k-sequent 264 | basic_os=dynix 265 | ;; 266 | blackfin) 267 | basic_machine=bfin-unknown 268 | basic_os=linux 269 | ;; 270 | cegcc) 271 | basic_machine=arm-unknown 272 | basic_os=cegcc 273 | ;; 274 | convex-c1) 275 | basic_machine=c1-convex 276 | basic_os=bsd 277 | ;; 278 | convex-c2) 279 | basic_machine=c2-convex 280 | basic_os=bsd 281 | ;; 282 | convex-c32) 283 | basic_machine=c32-convex 284 | basic_os=bsd 285 | ;; 286 | convex-c34) 287 | basic_machine=c34-convex 288 | basic_os=bsd 289 | ;; 290 | convex-c38) 291 | basic_machine=c38-convex 292 | basic_os=bsd 293 | ;; 294 | cray) 295 | basic_machine=j90-cray 296 | basic_os=unicos 297 | ;; 298 | crds | unos) 299 | basic_machine=m68k-crds 300 | basic_os= 301 | ;; 302 | da30) 303 | basic_machine=m68k-da30 304 | basic_os= 305 | ;; 306 | decstation | pmax | pmin | dec3100 | decstatn) 307 | basic_machine=mips-dec 308 | basic_os= 309 | ;; 310 | delta88) 311 | basic_machine=m88k-motorola 312 | basic_os=sysv3 313 | ;; 314 | dicos) 315 | basic_machine=i686-pc 316 | basic_os=dicos 317 | ;; 318 | djgpp) 319 | basic_machine=i586-pc 320 | basic_os=msdosdjgpp 321 | ;; 322 | ebmon29k) 323 | basic_machine=a29k-amd 324 | basic_os=ebmon 325 | ;; 326 | es1800 | OSE68k | ose68k | ose | OSE) 327 | basic_machine=m68k-ericsson 328 | basic_os=ose 329 | ;; 330 | gmicro) 331 | basic_machine=tron-gmicro 332 | basic_os=sysv 333 | ;; 334 | go32) 335 | basic_machine=i386-pc 336 | basic_os=go32 337 | ;; 338 | h8300hms) 339 | basic_machine=h8300-hitachi 340 | basic_os=hms 341 | ;; 342 | h8300xray) 343 | basic_machine=h8300-hitachi 344 | basic_os=xray 345 | ;; 346 | h8500hms) 347 | basic_machine=h8500-hitachi 348 | basic_os=hms 349 | ;; 350 | harris) 351 | basic_machine=m88k-harris 352 | basic_os=sysv3 353 | ;; 354 | hp300 | hp300hpux) 355 | basic_machine=m68k-hp 356 | basic_os=hpux 357 | ;; 358 | hp300bsd) 359 | basic_machine=m68k-hp 360 | basic_os=bsd 361 | ;; 362 | hppaosf) 363 | basic_machine=hppa1.1-hp 364 | basic_os=osf 365 | ;; 366 | hppro) 367 | basic_machine=hppa1.1-hp 368 | basic_os=proelf 369 | ;; 370 | i386mach) 371 | basic_machine=i386-mach 372 | basic_os=mach 373 | ;; 374 | isi68 | isi) 375 | basic_machine=m68k-isi 376 | basic_os=sysv 377 | ;; 378 | m68knommu) 379 | basic_machine=m68k-unknown 380 | basic_os=linux 381 | ;; 382 | magnum | m3230) 383 | basic_machine=mips-mips 384 | basic_os=sysv 385 | ;; 386 | merlin) 387 | basic_machine=ns32k-utek 388 | basic_os=sysv 389 | ;; 390 | mingw64) 391 | basic_machine=x86_64-pc 392 | basic_os=mingw64 393 | ;; 394 | mingw32) 395 | basic_machine=i686-pc 396 | basic_os=mingw32 397 | ;; 398 | mingw32ce) 399 | basic_machine=arm-unknown 400 | basic_os=mingw32ce 401 | ;; 402 | monitor) 403 | basic_machine=m68k-rom68k 404 | basic_os=coff 405 | ;; 406 | morphos) 407 | basic_machine=powerpc-unknown 408 | basic_os=morphos 409 | ;; 410 | moxiebox) 411 | basic_machine=moxie-unknown 412 | basic_os=moxiebox 413 | ;; 414 | msdos) 415 | basic_machine=i386-pc 416 | basic_os=msdos 417 | ;; 418 | msys) 419 | basic_machine=i686-pc 420 | basic_os=msys 421 | ;; 422 | mvs) 423 | basic_machine=i370-ibm 424 | basic_os=mvs 425 | ;; 426 | nacl) 427 | basic_machine=le32-unknown 428 | basic_os=nacl 429 | ;; 430 | ncr3000) 431 | basic_machine=i486-ncr 432 | basic_os=sysv4 433 | ;; 434 | netbsd386) 435 | basic_machine=i386-pc 436 | basic_os=netbsd 437 | ;; 438 | netwinder) 439 | basic_machine=armv4l-rebel 440 | basic_os=linux 441 | ;; 442 | news | news700 | news800 | news900) 443 | basic_machine=m68k-sony 444 | basic_os=newsos 445 | ;; 446 | news1000) 447 | basic_machine=m68030-sony 448 | basic_os=newsos 449 | ;; 450 | necv70) 451 | basic_machine=v70-nec 452 | basic_os=sysv 453 | ;; 454 | nh3000) 455 | basic_machine=m68k-harris 456 | basic_os=cxux 457 | ;; 458 | nh[45]000) 459 | basic_machine=m88k-harris 460 | basic_os=cxux 461 | ;; 462 | nindy960) 463 | basic_machine=i960-intel 464 | basic_os=nindy 465 | ;; 466 | mon960) 467 | basic_machine=i960-intel 468 | basic_os=mon960 469 | ;; 470 | nonstopux) 471 | basic_machine=mips-compaq 472 | basic_os=nonstopux 473 | ;; 474 | os400) 475 | basic_machine=powerpc-ibm 476 | basic_os=os400 477 | ;; 478 | OSE68000 | ose68000) 479 | basic_machine=m68000-ericsson 480 | basic_os=ose 481 | ;; 482 | os68k) 483 | basic_machine=m68k-none 484 | basic_os=os68k 485 | ;; 486 | paragon) 487 | basic_machine=i860-intel 488 | basic_os=osf 489 | ;; 490 | parisc) 491 | basic_machine=hppa-unknown 492 | basic_os=linux 493 | ;; 494 | psp) 495 | basic_machine=mipsallegrexel-sony 496 | basic_os=psp 497 | ;; 498 | pw32) 499 | basic_machine=i586-unknown 500 | basic_os=pw32 501 | ;; 502 | rdos | rdos64) 503 | basic_machine=x86_64-pc 504 | basic_os=rdos 505 | ;; 506 | rdos32) 507 | basic_machine=i386-pc 508 | basic_os=rdos 509 | ;; 510 | rom68k) 511 | basic_machine=m68k-rom68k 512 | basic_os=coff 513 | ;; 514 | sa29200) 515 | basic_machine=a29k-amd 516 | basic_os=udi 517 | ;; 518 | sei) 519 | basic_machine=mips-sei 520 | basic_os=seiux 521 | ;; 522 | sequent) 523 | basic_machine=i386-sequent 524 | basic_os= 525 | ;; 526 | sps7) 527 | basic_machine=m68k-bull 528 | basic_os=sysv2 529 | ;; 530 | st2000) 531 | basic_machine=m68k-tandem 532 | basic_os= 533 | ;; 534 | stratus) 535 | basic_machine=i860-stratus 536 | basic_os=sysv4 537 | ;; 538 | sun2) 539 | basic_machine=m68000-sun 540 | basic_os= 541 | ;; 542 | sun2os3) 543 | basic_machine=m68000-sun 544 | basic_os=sunos3 545 | ;; 546 | sun2os4) 547 | basic_machine=m68000-sun 548 | basic_os=sunos4 549 | ;; 550 | sun3) 551 | basic_machine=m68k-sun 552 | basic_os= 553 | ;; 554 | sun3os3) 555 | basic_machine=m68k-sun 556 | basic_os=sunos3 557 | ;; 558 | sun3os4) 559 | basic_machine=m68k-sun 560 | basic_os=sunos4 561 | ;; 562 | sun4) 563 | basic_machine=sparc-sun 564 | basic_os= 565 | ;; 566 | sun4os3) 567 | basic_machine=sparc-sun 568 | basic_os=sunos3 569 | ;; 570 | sun4os4) 571 | basic_machine=sparc-sun 572 | basic_os=sunos4 573 | ;; 574 | sun4sol2) 575 | basic_machine=sparc-sun 576 | basic_os=solaris2 577 | ;; 578 | sun386 | sun386i | roadrunner) 579 | basic_machine=i386-sun 580 | basic_os= 581 | ;; 582 | sv1) 583 | basic_machine=sv1-cray 584 | basic_os=unicos 585 | ;; 586 | symmetry) 587 | basic_machine=i386-sequent 588 | basic_os=dynix 589 | ;; 590 | t3e) 591 | basic_machine=alphaev5-cray 592 | basic_os=unicos 593 | ;; 594 | t90) 595 | basic_machine=t90-cray 596 | basic_os=unicos 597 | ;; 598 | toad1) 599 | basic_machine=pdp10-xkl 600 | basic_os=tops20 601 | ;; 602 | tpf) 603 | basic_machine=s390x-ibm 604 | basic_os=tpf 605 | ;; 606 | udi29k) 607 | basic_machine=a29k-amd 608 | basic_os=udi 609 | ;; 610 | ultra3) 611 | basic_machine=a29k-nyu 612 | basic_os=sym1 613 | ;; 614 | v810 | necv810) 615 | basic_machine=v810-nec 616 | basic_os=none 617 | ;; 618 | vaxv) 619 | basic_machine=vax-dec 620 | basic_os=sysv 621 | ;; 622 | vms) 623 | basic_machine=vax-dec 624 | basic_os=vms 625 | ;; 626 | vsta) 627 | basic_machine=i386-pc 628 | basic_os=vsta 629 | ;; 630 | vxworks960) 631 | basic_machine=i960-wrs 632 | basic_os=vxworks 633 | ;; 634 | vxworks68) 635 | basic_machine=m68k-wrs 636 | basic_os=vxworks 637 | ;; 638 | vxworks29k) 639 | basic_machine=a29k-wrs 640 | basic_os=vxworks 641 | ;; 642 | xbox) 643 | basic_machine=i686-pc 644 | basic_os=mingw32 645 | ;; 646 | ymp) 647 | basic_machine=ymp-cray 648 | basic_os=unicos 649 | ;; 650 | *) 651 | basic_machine=$1 652 | basic_os= 653 | ;; 654 | esac 655 | ;; 656 | esac 657 | 658 | # Decode 1-component or ad-hoc basic machines 659 | case $basic_machine in 660 | # Here we handle the default manufacturer of certain CPU types. It is in 661 | # some cases the only manufacturer, in others, it is the most popular. 662 | w89k) 663 | cpu=hppa1.1 664 | vendor=winbond 665 | ;; 666 | op50n) 667 | cpu=hppa1.1 668 | vendor=oki 669 | ;; 670 | op60c) 671 | cpu=hppa1.1 672 | vendor=oki 673 | ;; 674 | ibm*) 675 | cpu=i370 676 | vendor=ibm 677 | ;; 678 | orion105) 679 | cpu=clipper 680 | vendor=highlevel 681 | ;; 682 | mac | mpw | mac-mpw) 683 | cpu=m68k 684 | vendor=apple 685 | ;; 686 | pmac | pmac-mpw) 687 | cpu=powerpc 688 | vendor=apple 689 | ;; 690 | 691 | # Recognize the various machine names and aliases which stand 692 | # for a CPU type and a company and sometimes even an OS. 693 | 3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc) 694 | cpu=m68000 695 | vendor=att 696 | ;; 697 | 3b*) 698 | cpu=we32k 699 | vendor=att 700 | ;; 701 | bluegene*) 702 | cpu=powerpc 703 | vendor=ibm 704 | basic_os=cnk 705 | ;; 706 | decsystem10* | dec10*) 707 | cpu=pdp10 708 | vendor=dec 709 | basic_os=tops10 710 | ;; 711 | decsystem20* | dec20*) 712 | cpu=pdp10 713 | vendor=dec 714 | basic_os=tops20 715 | ;; 716 | delta | 3300 | motorola-3300 | motorola-delta \ 717 | | 3300-motorola | delta-motorola) 718 | cpu=m68k 719 | vendor=motorola 720 | ;; 721 | dpx2*) 722 | cpu=m68k 723 | vendor=bull 724 | basic_os=sysv3 725 | ;; 726 | encore | umax | mmax) 727 | cpu=ns32k 728 | vendor=encore 729 | ;; 730 | elxsi) 731 | cpu=elxsi 732 | vendor=elxsi 733 | basic_os=${basic_os:-bsd} 734 | ;; 735 | fx2800) 736 | cpu=i860 737 | vendor=alliant 738 | ;; 739 | genix) 740 | cpu=ns32k 741 | vendor=ns 742 | ;; 743 | h3050r* | hiux*) 744 | cpu=hppa1.1 745 | vendor=hitachi 746 | basic_os=hiuxwe2 747 | ;; 748 | hp3k9[0-9][0-9] | hp9[0-9][0-9]) 749 | cpu=hppa1.0 750 | vendor=hp 751 | ;; 752 | hp9k2[0-9][0-9] | hp9k31[0-9]) 753 | cpu=m68000 754 | vendor=hp 755 | ;; 756 | hp9k3[2-9][0-9]) 757 | cpu=m68k 758 | vendor=hp 759 | ;; 760 | hp9k6[0-9][0-9] | hp6[0-9][0-9]) 761 | cpu=hppa1.0 762 | vendor=hp 763 | ;; 764 | hp9k7[0-79][0-9] | hp7[0-79][0-9]) 765 | cpu=hppa1.1 766 | vendor=hp 767 | ;; 768 | hp9k78[0-9] | hp78[0-9]) 769 | # FIXME: really hppa2.0-hp 770 | cpu=hppa1.1 771 | vendor=hp 772 | ;; 773 | hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893) 774 | # FIXME: really hppa2.0-hp 775 | cpu=hppa1.1 776 | vendor=hp 777 | ;; 778 | hp9k8[0-9][13679] | hp8[0-9][13679]) 779 | cpu=hppa1.1 780 | vendor=hp 781 | ;; 782 | hp9k8[0-9][0-9] | hp8[0-9][0-9]) 783 | cpu=hppa1.0 784 | vendor=hp 785 | ;; 786 | i*86v32) 787 | cpu=`echo "$1" | sed -e 's/86.*/86/'` 788 | vendor=pc 789 | basic_os=sysv32 790 | ;; 791 | i*86v4*) 792 | cpu=`echo "$1" | sed -e 's/86.*/86/'` 793 | vendor=pc 794 | basic_os=sysv4 795 | ;; 796 | i*86v) 797 | cpu=`echo "$1" | sed -e 's/86.*/86/'` 798 | vendor=pc 799 | basic_os=sysv 800 | ;; 801 | i*86sol2) 802 | cpu=`echo "$1" | sed -e 's/86.*/86/'` 803 | vendor=pc 804 | basic_os=solaris2 805 | ;; 806 | j90 | j90-cray) 807 | cpu=j90 808 | vendor=cray 809 | basic_os=${basic_os:-unicos} 810 | ;; 811 | iris | iris4d) 812 | cpu=mips 813 | vendor=sgi 814 | case $basic_os in 815 | irix*) 816 | ;; 817 | *) 818 | basic_os=irix4 819 | ;; 820 | esac 821 | ;; 822 | miniframe) 823 | cpu=m68000 824 | vendor=convergent 825 | ;; 826 | *mint | mint[0-9]* | *MiNT | *MiNT[0-9]*) 827 | cpu=m68k 828 | vendor=atari 829 | basic_os=mint 830 | ;; 831 | news-3600 | risc-news) 832 | cpu=mips 833 | vendor=sony 834 | basic_os=newsos 835 | ;; 836 | next | m*-next) 837 | cpu=m68k 838 | vendor=next 839 | case $basic_os in 840 | openstep*) 841 | ;; 842 | nextstep*) 843 | ;; 844 | ns2*) 845 | basic_os=nextstep2 846 | ;; 847 | *) 848 | basic_os=nextstep3 849 | ;; 850 | esac 851 | ;; 852 | np1) 853 | cpu=np1 854 | vendor=gould 855 | ;; 856 | op50n-* | op60c-*) 857 | cpu=hppa1.1 858 | vendor=oki 859 | basic_os=proelf 860 | ;; 861 | pa-hitachi) 862 | cpu=hppa1.1 863 | vendor=hitachi 864 | basic_os=hiuxwe2 865 | ;; 866 | pbd) 867 | cpu=sparc 868 | vendor=tti 869 | ;; 870 | pbb) 871 | cpu=m68k 872 | vendor=tti 873 | ;; 874 | pc532) 875 | cpu=ns32k 876 | vendor=pc532 877 | ;; 878 | pn) 879 | cpu=pn 880 | vendor=gould 881 | ;; 882 | power) 883 | cpu=power 884 | vendor=ibm 885 | ;; 886 | ps2) 887 | cpu=i386 888 | vendor=ibm 889 | ;; 890 | rm[46]00) 891 | cpu=mips 892 | vendor=siemens 893 | ;; 894 | rtpc | rtpc-*) 895 | cpu=romp 896 | vendor=ibm 897 | ;; 898 | sde) 899 | cpu=mipsisa32 900 | vendor=sde 901 | basic_os=${basic_os:-elf} 902 | ;; 903 | simso-wrs) 904 | cpu=sparclite 905 | vendor=wrs 906 | basic_os=vxworks 907 | ;; 908 | tower | tower-32) 909 | cpu=m68k 910 | vendor=ncr 911 | ;; 912 | vpp*|vx|vx-*) 913 | cpu=f301 914 | vendor=fujitsu 915 | ;; 916 | w65) 917 | cpu=w65 918 | vendor=wdc 919 | ;; 920 | w89k-*) 921 | cpu=hppa1.1 922 | vendor=winbond 923 | basic_os=proelf 924 | ;; 925 | none) 926 | cpu=none 927 | vendor=none 928 | ;; 929 | leon|leon[3-9]) 930 | cpu=sparc 931 | vendor=$basic_machine 932 | ;; 933 | leon-*|leon[3-9]-*) 934 | cpu=sparc 935 | vendor=`echo "$basic_machine" | sed 's/-.*//'` 936 | ;; 937 | 938 | *-*) 939 | # shellcheck disable=SC2162 940 | saved_IFS=$IFS 941 | IFS="-" read cpu vendor <&2 1289 | exit 1 1290 | ;; 1291 | esac 1292 | ;; 1293 | esac 1294 | 1295 | # Here we canonicalize certain aliases for manufacturers. 1296 | case $vendor in 1297 | digital*) 1298 | vendor=dec 1299 | ;; 1300 | commodore*) 1301 | vendor=cbm 1302 | ;; 1303 | *) 1304 | ;; 1305 | esac 1306 | 1307 | # Decode manufacturer-specific aliases for certain operating systems. 1308 | 1309 | if test x$basic_os != x 1310 | then 1311 | 1312 | # First recognize some ad-hoc cases, or perhaps split kernel-os, or else just 1313 | # set os. 1314 | case $basic_os in 1315 | gnu/linux*) 1316 | kernel=linux 1317 | os=`echo "$basic_os" | sed -e 's|gnu/linux|gnu|'` 1318 | ;; 1319 | os2-emx) 1320 | kernel=os2 1321 | os=`echo "$basic_os" | sed -e 's|os2-emx|emx|'` 1322 | ;; 1323 | nto-qnx*) 1324 | kernel=nto 1325 | os=`echo "$basic_os" | sed -e 's|nto-qnx|qnx|'` 1326 | ;; 1327 | *-*) 1328 | # shellcheck disable=SC2162 1329 | saved_IFS=$IFS 1330 | IFS="-" read kernel os <&2 1767 | exit 1 1768 | ;; 1769 | esac 1770 | 1771 | # As a final step for OS-related things, validate the OS-kernel combination 1772 | # (given a valid OS), if there is a kernel. 1773 | case $kernel-$os in 1774 | linux-gnu* | linux-dietlibc* | linux-android* | linux-newlib* \ 1775 | | linux-musl* | linux-relibc* | linux-uclibc* ) 1776 | ;; 1777 | uclinux-uclibc* ) 1778 | ;; 1779 | -dietlibc* | -newlib* | -musl* | -relibc* | -uclibc* ) 1780 | # These are just libc implementations, not actual OSes, and thus 1781 | # require a kernel. 1782 | echo "Invalid configuration \`$1': libc \`$os' needs explicit kernel." 1>&2 1783 | exit 1 1784 | ;; 1785 | kfreebsd*-gnu* | kopensolaris*-gnu*) 1786 | ;; 1787 | vxworks-simlinux | vxworks-simwindows | vxworks-spe) 1788 | ;; 1789 | nto-qnx*) 1790 | ;; 1791 | os2-emx) 1792 | ;; 1793 | *-eabi* | *-gnueabi*) 1794 | ;; 1795 | -*) 1796 | # Blank kernel with real OS is always fine. 1797 | ;; 1798 | *-*) 1799 | echo "Invalid configuration \`$1': Kernel \`$kernel' not known to work with OS \`$os'." 1>&2 1800 | exit 1 1801 | ;; 1802 | esac 1803 | 1804 | # Here we handle the case where we know the os, and the CPU type, but not the 1805 | # manufacturer. We pick the logical manufacturer. 1806 | case $vendor in 1807 | unknown) 1808 | case $cpu-$os in 1809 | *-riscix*) 1810 | vendor=acorn 1811 | ;; 1812 | *-sunos*) 1813 | vendor=sun 1814 | ;; 1815 | *-cnk* | *-aix*) 1816 | vendor=ibm 1817 | ;; 1818 | *-beos*) 1819 | vendor=be 1820 | ;; 1821 | *-hpux*) 1822 | vendor=hp 1823 | ;; 1824 | *-mpeix*) 1825 | vendor=hp 1826 | ;; 1827 | *-hiux*) 1828 | vendor=hitachi 1829 | ;; 1830 | *-unos*) 1831 | vendor=crds 1832 | ;; 1833 | *-dgux*) 1834 | vendor=dg 1835 | ;; 1836 | *-luna*) 1837 | vendor=omron 1838 | ;; 1839 | *-genix*) 1840 | vendor=ns 1841 | ;; 1842 | *-clix*) 1843 | vendor=intergraph 1844 | ;; 1845 | *-mvs* | *-opened*) 1846 | vendor=ibm 1847 | ;; 1848 | *-os400*) 1849 | vendor=ibm 1850 | ;; 1851 | s390-* | s390x-*) 1852 | vendor=ibm 1853 | ;; 1854 | *-ptx*) 1855 | vendor=sequent 1856 | ;; 1857 | *-tpf*) 1858 | vendor=ibm 1859 | ;; 1860 | *-vxsim* | *-vxworks* | *-windiss*) 1861 | vendor=wrs 1862 | ;; 1863 | *-aux*) 1864 | vendor=apple 1865 | ;; 1866 | *-hms*) 1867 | vendor=hitachi 1868 | ;; 1869 | *-mpw* | *-macos*) 1870 | vendor=apple 1871 | ;; 1872 | *-*mint | *-mint[0-9]* | *-*MiNT | *-MiNT[0-9]*) 1873 | vendor=atari 1874 | ;; 1875 | *-vos*) 1876 | vendor=stratus 1877 | ;; 1878 | esac 1879 | ;; 1880 | esac 1881 | 1882 | echo "$cpu-$vendor-${kernel:+$kernel-}$os" 1883 | exit 1884 | 1885 | # Local variables: 1886 | # eval: (add-hook 'before-save-hook 'time-stamp) 1887 | # time-stamp-start: "timestamp='" 1888 | # time-stamp-format: "%:y-%02m-%02d" 1889 | # time-stamp-end: "'" 1890 | # End: 1891 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_PREREQ(2.68) 2 | AC_INIT([try], [0.2.0], [https://github.com/binpash/try/issues]) 3 | 4 | # make sure we're in the right place 5 | AC_CONFIG_SRCDIR([try]) 6 | AC_REQUIRE_AUX_FILE([docs/try.1.md]) 7 | 8 | AC_ARG_ENABLE([utils], 9 | [AS_HELP_STRING([--disable-utils], 10 | [don't compile C utilities for summarizing and committing changes (default is yes)])], 11 | [enable_utils=${enableval}], [enable_utils=yes]) 12 | 13 | if test "$enable_utils" = "yes" 14 | then 15 | AC_REQUIRE_AUX_FILE([utils/try-commit.c]) 16 | 17 | # build tools 18 | AC_PROG_CC 19 | 20 | # CFLAGS and CPPFLAGGS 21 | if test -z "$CFLAGS" 22 | then 23 | AUTO_CFLAGS="-g -Wall -O2" 24 | else 25 | AUTO_CFLAGS="" 26 | fi 27 | AUTO_CPPFLAGS="" 28 | 29 | CFLAGS=${CFLAGS-"$AUTO_CFLAGS"} 30 | CPPFLAGS=${CPPFLAGS-"$AUTO_CPPFLAGS"} 31 | 32 | AC_SUBST(CFLAGS) 33 | AC_SUBST(CPPFLAGS) 34 | 35 | # needed C types 36 | AC_TYPE_PID_T 37 | AC_TYPE_SIZE_T 38 | AC_TYPE_SSIZE_T 39 | 40 | # needed C functions 41 | AC_FUNC_MALLOC 42 | AC_FUNC_REALLOC 43 | AC_FUNC_FORK 44 | AC_FUNC_LSTAT_FOLLOWS_SLASHED_SYMLINK 45 | 46 | AC_CHECK_HEADERS([fcntl.h unistd.h fts.h]) 47 | AC_CHECK_FUNCS([regcomp perror strerror getline getxattr fts_open]) 48 | 49 | missing_funs="" 50 | for fun in regcomp perror strerror getline getxattr fts_open 51 | do 52 | varname="\$ac_cv_func_$fun" 53 | if test "$(eval "echo ${varname-no}")" != "yes" 54 | then 55 | missing_funs="$missing_funs $fun" 56 | fi 57 | done 58 | 59 | 60 | if ! test -z "$missing_funs" 61 | then 62 | AC_MSG_NOTICE([ 63 | ------------------------------------------------------------------------ 64 | WARNING: 65 | 66 | Some critical functions are missing, so utilities cannot be compiled. 67 | Missing functions:$missing_funs 68 | 69 | Continuing as if --disable-utils was passed. 70 | ------------------------------------------------------------------------ 71 | ]); 72 | enable_utils=no 73 | fi 74 | fi 75 | AC_SUBST(enable_utils) 76 | 77 | # install 78 | AC_PROG_INSTALL 79 | 80 | dnl TRY_REQUIRE_PROG(progname, checking_msg, pre, true_if_failed, post, fail_msg) 81 | AC_DEFUN([TRY_REQUIRE_PROG], [ 82 | AC_MSG_CHECKING([ifelse([$2], [], [for $1], [$2])]) 83 | $3 84 | if ifelse([$4], [], [! type -p $1 >/dev/null 2>&1], [$4]) 85 | then 86 | AC_MSG_RESULT([no]) 87 | AC_MSG_ERROR([try needs $1 to work, but ifelse([$6], [], [it could not be found], [$6]).]) 88 | else 89 | AC_MSG_RESULT([yes]) 90 | $5 91 | fi 92 | ]) 93 | 94 | # symlinks 95 | AC_PROG_LN_S 96 | if test "$LN_S" != "ln -s" 97 | then 98 | AC_MSG_NOTICE([ 99 | ---------------------------------------------------------------------- 100 | WARNING 101 | 102 | Your system seems to not have a working implementation of symbolic 103 | links. `try` may behave strangely. 104 | ---------------------------------------------------------------------- 105 | ]) 106 | fi 107 | 108 | #mktemp 109 | TRY_REQUIRE_PROG([mktemp]) 110 | TRY_REQUIRE_PROG([mktemp -d], [whether mktemp -d works], [d=$(mktemp -d 2>/dev/null)], [test "$?" != 0 || ! test -d "$d"], [rmdir "$d"], [your mktemp does not support the -d option]) 111 | 112 | TRY_REQUIRE_PROG([df]) 113 | TRY_REQUIRE_PROG([find]) 114 | TRY_REQUIRE_PROG([grep]) 115 | TRY_REQUIRE_PROG([findmnt]) 116 | TRY_REQUIRE_PROG([sort]) 117 | TRY_REQUIRE_PROG([getfattr]) 118 | 119 | TRY_REQUIRE_PROG([overlayfs],[for overlayfs],[],[! lsmod | grep -q overlay], [], [the overlay module did not appear in the output of lsmod]) 120 | 121 | AC_CHECK_PROG([mergerfs], [mergerfs], [yes], [no]) 122 | AC_CHECK_PROG([unionfs], [unionfs], [yes], [no]) 123 | 124 | AC_MSG_CHECKING([for union helpers (unionfs or mergerfs)]) 125 | if test "$ac_cv_prog_mergerfs" = "no" && test "$ac_cv_prog_unionfs" = "no" 126 | then 127 | AC_MSG_RESULT([no; try may issue warnings and error messages]) 128 | else 129 | AC_MSG_RESULT([yes]) 130 | fi 131 | 132 | TRY_REQUIRE_PROG([readlink]) 133 | TRY_REQUIRE_PROG([unshare], [for unshare], [ 134 | res=$(unshare --mount --map-root-user --user --pid --fork -- ls $PWD/try 2>/dev/null) 135 | ], [ 136 | test "$?" != 0 || test "$res" != "$PWD/try" 137 | ], [], [could not run unshare]) 138 | 139 | # for manpages 140 | if test -f man/try.1.gz 141 | then 142 | need_manpage=no 143 | else 144 | need_manpage=yes 145 | fi 146 | AC_ARG_ENABLE([manpage], 147 | [AS_HELP_STRING([--enable-manpage], 148 | [generate the manpage instead of using the cached version (requires pandoc, default is no when man/try.1.gz is present)])], 149 | [enable_manpage=${enableval}], [enable_manpage=${need_manpage}]) 150 | if test "$enable_manpage" = "yes" 151 | then 152 | AC_MSG_CHECKING([for pandoc (for manpage)]) 153 | if ! type -p pandoc >/dev/null 2>&1 154 | then 155 | AC_MSG_RESULT([no]) 156 | enable_manpage="no" 157 | else 158 | AC_MSG_RESULT([yes]) 159 | fi 160 | 161 | AC_MSG_CHECKING([for gzip (for manpage)]) 162 | if ! type -p gzip >/dev/null 2>&1 163 | then 164 | AC_MSG_RESULT([no]) 165 | enable_manpage="no" 166 | else 167 | AC_MSG_RESULT([yes]) 168 | fi 169 | 170 | if test "$enable_manpage" = no 171 | then 172 | if test -f man/try.1.gz 173 | then 174 | AC_MSG_NOTICE([ 175 | ------------------------------------------------------------------------ 176 | WARNING 177 | 178 | Your system is missing some programs used to generate the manpage, so 179 | the cached version will be used instead. 180 | ------------------------------------------------------------------------ 181 | ]) 182 | else 183 | AC_MSG_ERROR([ 184 | ------------------------------------------------------------------------ 185 | WARNING 186 | 187 | Your system is missing some programs used to generate the manpage and 188 | no cached manpage is available. 189 | ------------------------------------------------------------------------ 190 | ]) 191 | fi 192 | fi 193 | fi 194 | AC_SUBST(enable_manpage) 195 | 196 | 197 | # programs just used in tests 198 | missing_test_programs=0 199 | 200 | AC_PROG_AWK 201 | AC_MSG_CHECKING([whether awk is gawk (for tests)]) 202 | if test "$(stat -L -c %i "$(type -p "$AWK")")" != "$(stat -L -c %i "$(type -p awk)")" 203 | then 204 | AC_MSG_RESULT([no; some tests may behave strangely]) 205 | : $((missing_test_programs += 1)) 206 | else 207 | AC_MSG_RESULT([yes]) 208 | fi 209 | 210 | dnl TRY_REQUIRE_PROG(progname) 211 | AC_DEFUN([TRY_TEST_PROG], [ 212 | AC_MSG_CHECKING([for $1 (for tests)]) 213 | if ! type -p $1 >/dev/null 2>&1 214 | then 215 | AC_MSG_RESULT([no; some tests may fail]) 216 | : $((missing_test_programs += 1)) 217 | else 218 | AC_MSG_RESULT([yes]) 219 | fi 220 | ]) 221 | 222 | TRY_TEST_PROG([expect]) 223 | TRY_TEST_PROG([curl]) 224 | TRY_TEST_PROG([diff]) 225 | TRY_TEST_PROG([touch]) 226 | TRY_TEST_PROG([gunzip]) 227 | 228 | if test "$missing_test_programs" -ne 0 229 | then 230 | AC_MSG_NOTICE([ 231 | ------------------------------------------------------------------------ 232 | WARNING 233 | 234 | Your system is missing some programs used in tests, so some tests may 235 | fail. 236 | ------------------------------------------------------------------------ 237 | ]) 238 | 239 | fi 240 | 241 | AC_CONFIG_FILES([Makefile:Makefile.in]) 242 | AC_OUTPUT 243 | 244 | cat <&2 148 | exit 1;; 149 | esac 150 | shift;; 151 | 152 | -o) chowncmd="$chownprog $2" 153 | shift;; 154 | 155 | -p) cpprog="$cpprog -p";; 156 | 157 | -s) stripcmd=$stripprog;; 158 | 159 | -S) backupsuffix="$2" 160 | shift;; 161 | 162 | -t) 163 | is_target_a_directory=always 164 | dst_arg=$2 165 | # Protect names problematic for 'test' and other utilities. 166 | case $dst_arg in 167 | -* | [=\(\)!]) dst_arg=./$dst_arg;; 168 | esac 169 | shift;; 170 | 171 | -T) is_target_a_directory=never;; 172 | 173 | --version) echo "$0 $scriptversion"; exit $?;; 174 | 175 | --) shift 176 | break;; 177 | 178 | -*) echo "$0: invalid option: $1" >&2 179 | exit 1;; 180 | 181 | *) break;; 182 | esac 183 | shift 184 | done 185 | 186 | # We allow the use of options -d and -T together, by making -d 187 | # take the precedence; this is for compatibility with GNU install. 188 | 189 | if test -n "$dir_arg"; then 190 | if test -n "$dst_arg"; then 191 | echo "$0: target directory not allowed when installing a directory." >&2 192 | exit 1 193 | fi 194 | fi 195 | 196 | if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then 197 | # When -d is used, all remaining arguments are directories to create. 198 | # When -t is used, the destination is already specified. 199 | # Otherwise, the last argument is the destination. Remove it from $@. 200 | for arg 201 | do 202 | if test -n "$dst_arg"; then 203 | # $@ is not empty: it contains at least $arg. 204 | set fnord "$@" "$dst_arg" 205 | shift # fnord 206 | fi 207 | shift # arg 208 | dst_arg=$arg 209 | # Protect names problematic for 'test' and other utilities. 210 | case $dst_arg in 211 | -* | [=\(\)!]) dst_arg=./$dst_arg;; 212 | esac 213 | done 214 | fi 215 | 216 | if test $# -eq 0; then 217 | if test -z "$dir_arg"; then 218 | echo "$0: no input file specified." >&2 219 | exit 1 220 | fi 221 | # It's OK to call 'install-sh -d' without argument. 222 | # This can happen when creating conditional directories. 223 | exit 0 224 | fi 225 | 226 | if test -z "$dir_arg"; then 227 | if test $# -gt 1 || test "$is_target_a_directory" = always; then 228 | if test ! -d "$dst_arg"; then 229 | echo "$0: $dst_arg: Is not a directory." >&2 230 | exit 1 231 | fi 232 | fi 233 | fi 234 | 235 | if test -z "$dir_arg"; then 236 | do_exit='(exit $ret); exit $ret' 237 | trap "ret=129; $do_exit" 1 238 | trap "ret=130; $do_exit" 2 239 | trap "ret=141; $do_exit" 13 240 | trap "ret=143; $do_exit" 15 241 | 242 | # Set umask so as not to create temps with too-generous modes. 243 | # However, 'strip' requires both read and write access to temps. 244 | case $mode in 245 | # Optimize common cases. 246 | *644) cp_umask=133;; 247 | *755) cp_umask=22;; 248 | 249 | *[0-7]) 250 | if test -z "$stripcmd"; then 251 | u_plus_rw= 252 | else 253 | u_plus_rw='% 200' 254 | fi 255 | cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; 256 | *) 257 | if test -z "$stripcmd"; then 258 | u_plus_rw= 259 | else 260 | u_plus_rw=,u+rw 261 | fi 262 | cp_umask=$mode$u_plus_rw;; 263 | esac 264 | fi 265 | 266 | for src 267 | do 268 | # Protect names problematic for 'test' and other utilities. 269 | case $src in 270 | -* | [=\(\)!]) src=./$src;; 271 | esac 272 | 273 | if test -n "$dir_arg"; then 274 | dst=$src 275 | dstdir=$dst 276 | test -d "$dstdir" 277 | dstdir_status=$? 278 | # Don't chown directories that already exist. 279 | if test $dstdir_status = 0; then 280 | chowncmd="" 281 | fi 282 | else 283 | 284 | # Waiting for this to be detected by the "$cpprog $src $dsttmp" command 285 | # might cause directories to be created, which would be especially bad 286 | # if $src (and thus $dsttmp) contains '*'. 287 | if test ! -f "$src" && test ! -d "$src"; then 288 | echo "$0: $src does not exist." >&2 289 | exit 1 290 | fi 291 | 292 | if test -z "$dst_arg"; then 293 | echo "$0: no destination specified." >&2 294 | exit 1 295 | fi 296 | dst=$dst_arg 297 | 298 | # If destination is a directory, append the input filename. 299 | if test -d "$dst"; then 300 | if test "$is_target_a_directory" = never; then 301 | echo "$0: $dst_arg: Is a directory" >&2 302 | exit 1 303 | fi 304 | dstdir=$dst 305 | dstbase=`basename "$src"` 306 | case $dst in 307 | */) dst=$dst$dstbase;; 308 | *) dst=$dst/$dstbase;; 309 | esac 310 | dstdir_status=0 311 | else 312 | dstdir=`dirname "$dst"` 313 | test -d "$dstdir" 314 | dstdir_status=$? 315 | fi 316 | fi 317 | 318 | case $dstdir in 319 | */) dstdirslash=$dstdir;; 320 | *) dstdirslash=$dstdir/;; 321 | esac 322 | 323 | obsolete_mkdir_used=false 324 | 325 | if test $dstdir_status != 0; then 326 | case $posix_mkdir in 327 | '') 328 | # With -d, create the new directory with the user-specified mode. 329 | # Otherwise, rely on $mkdir_umask. 330 | if test -n "$dir_arg"; then 331 | mkdir_mode=-m$mode 332 | else 333 | mkdir_mode= 334 | fi 335 | 336 | posix_mkdir=false 337 | # The $RANDOM variable is not portable (e.g., dash). Use it 338 | # here however when possible just to lower collision chance. 339 | tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ 340 | 341 | trap ' 342 | ret=$? 343 | rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null 344 | exit $ret 345 | ' 0 346 | 347 | # Because "mkdir -p" follows existing symlinks and we likely work 348 | # directly in world-writeable /tmp, make sure that the '$tmpdir' 349 | # directory is successfully created first before we actually test 350 | # 'mkdir -p'. 351 | if (umask $mkdir_umask && 352 | $mkdirprog $mkdir_mode "$tmpdir" && 353 | exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1 354 | then 355 | if test -z "$dir_arg" || { 356 | # Check for POSIX incompatibilities with -m. 357 | # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or 358 | # other-writable bit of parent directory when it shouldn't. 359 | # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. 360 | test_tmpdir="$tmpdir/a" 361 | ls_ld_tmpdir=`ls -ld "$test_tmpdir"` 362 | case $ls_ld_tmpdir in 363 | d????-?r-*) different_mode=700;; 364 | d????-?--*) different_mode=755;; 365 | *) false;; 366 | esac && 367 | $mkdirprog -m$different_mode -p -- "$test_tmpdir" && { 368 | ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"` 369 | test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" 370 | } 371 | } 372 | then posix_mkdir=: 373 | fi 374 | rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 375 | else 376 | # Remove any dirs left behind by ancient mkdir implementations. 377 | rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null 378 | fi 379 | trap '' 0;; 380 | esac 381 | 382 | if 383 | $posix_mkdir && ( 384 | umask $mkdir_umask && 385 | $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" 386 | ) 387 | then : 388 | else 389 | 390 | # mkdir does not conform to POSIX, 391 | # or it failed possibly due to a race condition. Create the 392 | # directory the slow way, step by step, checking for races as we go. 393 | 394 | case $dstdir in 395 | /*) prefix='/';; 396 | [-=\(\)!]*) prefix='./';; 397 | *) prefix='';; 398 | esac 399 | 400 | oIFS=$IFS 401 | IFS=/ 402 | set -f 403 | set fnord $dstdir 404 | shift 405 | set +f 406 | IFS=$oIFS 407 | 408 | prefixes= 409 | 410 | for d 411 | do 412 | test X"$d" = X && continue 413 | 414 | prefix=$prefix$d 415 | if test -d "$prefix"; then 416 | prefixes= 417 | else 418 | if $posix_mkdir; then 419 | (umask $mkdir_umask && 420 | $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break 421 | # Don't fail if two instances are running concurrently. 422 | test -d "$prefix" || exit 1 423 | else 424 | case $prefix in 425 | *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; 426 | *) qprefix=$prefix;; 427 | esac 428 | prefixes="$prefixes '$qprefix'" 429 | fi 430 | fi 431 | prefix=$prefix/ 432 | done 433 | 434 | if test -n "$prefixes"; then 435 | # Don't fail if two instances are running concurrently. 436 | (umask $mkdir_umask && 437 | eval "\$doit_exec \$mkdirprog $prefixes") || 438 | test -d "$dstdir" || exit 1 439 | obsolete_mkdir_used=true 440 | fi 441 | fi 442 | fi 443 | 444 | if test -n "$dir_arg"; then 445 | { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && 446 | { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && 447 | { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || 448 | test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 449 | else 450 | 451 | # Make a couple of temp file names in the proper directory. 452 | dsttmp=${dstdirslash}_inst.$$_ 453 | rmtmp=${dstdirslash}_rm.$$_ 454 | 455 | # Trap to clean up those temp files at exit. 456 | trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 457 | 458 | # Copy the file name to the temp name. 459 | (umask $cp_umask && 460 | { test -z "$stripcmd" || { 461 | # Create $dsttmp read-write so that cp doesn't create it read-only, 462 | # which would cause strip to fail. 463 | if test -z "$doit"; then 464 | : >"$dsttmp" # No need to fork-exec 'touch'. 465 | else 466 | $doit touch "$dsttmp" 467 | fi 468 | } 469 | } && 470 | $doit_exec $cpprog "$src" "$dsttmp") && 471 | 472 | # and set any options; do chmod last to preserve setuid bits. 473 | # 474 | # If any of these fail, we abort the whole thing. If we want to 475 | # ignore errors from any of these, just make sure not to ignore 476 | # errors from the above "$doit $cpprog $src $dsttmp" command. 477 | # 478 | { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && 479 | { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && 480 | { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && 481 | { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && 482 | 483 | # If -C, don't bother to copy if it wouldn't change the file. 484 | if $copy_on_change && 485 | old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && 486 | new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && 487 | set -f && 488 | set X $old && old=:$2:$4:$5:$6 && 489 | set X $new && new=:$2:$4:$5:$6 && 490 | set +f && 491 | test "$old" = "$new" && 492 | $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 493 | then 494 | rm -f "$dsttmp" 495 | else 496 | # If $backupsuffix is set, and the file being installed 497 | # already exists, attempt a backup. Don't worry if it fails, 498 | # e.g., if mv doesn't support -f. 499 | if test -n "$backupsuffix" && test -f "$dst"; then 500 | $doit $mvcmd -f "$dst" "$dst$backupsuffix" 2>/dev/null 501 | fi 502 | 503 | # Rename the file to the real destination. 504 | $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || 505 | 506 | # The rename failed, perhaps because mv can't rename something else 507 | # to itself, or perhaps because mv is so ancient that it does not 508 | # support -f. 509 | { 510 | # Now remove or move aside any old file at destination location. 511 | # We try this two ways since rm can't unlink itself on some 512 | # systems and the destination file might be busy for other 513 | # reasons. In this case, the final cleanup might fail but the new 514 | # file should still install successfully. 515 | { 516 | test ! -f "$dst" || 517 | $doit $rmcmd "$dst" 2>/dev/null || 518 | { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && 519 | { $doit $rmcmd "$rmtmp" 2>/dev/null; :; } 520 | } || 521 | { echo "$0: cannot unlink or rename $dst" >&2 522 | (exit 1); exit 1 523 | } 524 | } && 525 | 526 | # Now rename the file to the real destination. 527 | $doit $mvcmd "$dsttmp" "$dst" 528 | } 529 | fi || exit 1 530 | 531 | trap '' 0 532 | fi 533 | done 534 | 535 | # Local variables: 536 | # eval: (add-hook 'before-save-hook 'time-stamp) 537 | # time-stamp-start: "scriptversion=" 538 | # time-stamp-format: "%:y-%02m-%02d.%02H" 539 | # time-stamp-time-zone: "UTC0" 540 | # time-stamp-end: "; # UTC" 541 | # End: 542 | -------------------------------------------------------------------------------- /man/.gitignore: -------------------------------------------------------------------------------- 1 | try.1 2 | try.1.gz 3 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | autoreconfHook, 4 | lib, 5 | fetchFromGitHub, 6 | util-linux, 7 | mergerfs, 8 | attr, 9 | makeWrapper, 10 | pandoc, 11 | coreutils, 12 | installShellFiles, 13 | versionCheckHook, 14 | }: 15 | stdenv.mkDerivation { 16 | pname = "try"; 17 | version = "latest"; 18 | 19 | src = fetchFromGitHub { 20 | owner = "binpash"; 21 | repo = "try"; 22 | rev = "67052d8f20725f3cdc22ffaec33f7b7c14f1eb6b"; 23 | hash = "sha256-8mfCmqN50pRAeNTJUlRVrRQulWon4b2OL4Ug/ygBhB0="; 24 | }; 25 | 26 | # skip TRY_REQUIRE_PROG as it detects executable dependencies by running it 27 | postPatch = '' 28 | sed -i '/^AC_DEFUN(\[TRY_REQUIRE_PROG\]/,/^])$/c\AC_DEFUN([TRY_REQUIRE_PROG], [])' configure.ac 29 | ''; 30 | 31 | nativeBuildInputs = [ 32 | makeWrapper 33 | autoreconfHook 34 | pandoc 35 | installShellFiles 36 | ]; 37 | 38 | installPhase = '' 39 | runHook preInstall 40 | install -Dt $out/bin try 41 | install -Dt $out/bin utils/try-commit 42 | install -Dt $out/bin utils/try-summary 43 | wrapProgram $out/bin/try --prefix PATH : ${ 44 | lib.makeBinPath [ 45 | coreutils 46 | util-linux 47 | mergerfs 48 | attr 49 | ] 50 | } 51 | installManPage man/try.1.gz 52 | installShellCompletion --bash --name try.bash completions/try.bash 53 | runHook postInstall 54 | ''; 55 | 56 | doInstallCheck = true; 57 | nativeInstallCheckInputs = [ 58 | versionCheckHook 59 | ]; 60 | preVersionCheck = '' 61 | export version=0.2.0 62 | ''; 63 | versionCheckProgramArg = "-v"; 64 | 65 | meta = { 66 | homepage = "https://github.com/binpash/try"; 67 | description = "Lets you run a command and inspect its effects before changing your live system"; 68 | mainProgram = "try"; 69 | maintainers = with lib.maintainers; [ 70 | pasqui23 71 | ezrizhu 72 | ]; 73 | license = with lib.licenses; [ mit ]; 74 | platforms = lib.platforms.linux; 75 | }; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | This directory holds utility scripts used in development of `try`. 2 | 3 | The `lint.sh` script runs basic file linting. 4 | 5 | The `run_tests.sh` script runs the test suite. 6 | -------------------------------------------------------------------------------- /scripts/check_trycase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ################################################################################ 4 | # pluralize a word 5 | ################################################################################ 6 | 7 | plural() { 8 | if [ "$1" -eq 1 ] 9 | then 10 | echo "" 11 | else 12 | echo "s" 13 | fi 14 | } 15 | 16 | check_file() { 17 | trycase="$(mktemp -d)" 18 | 19 | for cf in file dir symlink opaque whiteout 20 | do 21 | mkdir "$trycase/$cf" 22 | for lf in file dir symlink nonexist 23 | do 24 | touch "$trycase/$cf/$lf" 25 | done 26 | done 27 | rm "$trycase/opaque/nonexist" # this case cannot occur 28 | 29 | ERRORS=0 30 | 31 | CASES=$(mktemp) 32 | grep -e "// TRYCASE" "$1" | cut -d'/' -f3 | cut -c10- >"$CASES" 33 | 34 | while IFS="" read -r c 35 | do 36 | cf="${c%,*}" 37 | lf="${c#*, }" 38 | lf="${lf%)}" 39 | 40 | case "$cf" in 41 | (file|dir|symlink|nonexist|opaque|whiteout);; 42 | (*) printf "ERROR: $1: invalid changed file: %s\n" "$cf" 43 | : $((ERRORS += 1)) 44 | ;; 45 | esac 46 | case "$lf" in 47 | (file|dir|symlink|nonexist|"*");; 48 | (*) printf "ERROR: $1: invalid local file: %s\n" "$lf" 49 | : $((ERRORS += 1)) 50 | ;; 51 | esac 52 | 53 | # shellcheck disable=SC2086 # deliberately letting $lf be *! 54 | rm "$trycase"/"$cf"/$lf 2>/dev/null 55 | done <"$CASES" 56 | rm "$CASES" 57 | 58 | # shellcheck disable=SC2045 # globs are annoying here 59 | for cf in $(ls "$trycase") 60 | do 61 | # shellcheck disable=SC2045 # globs are annoying here 62 | for lf in $(ls "$trycase/$cf") 63 | do 64 | printf "ERROR: $1: missing TRYCASE(%s, %s)\n" "$cf" "$lf" 65 | : $((ERRORS += 1)) 66 | done 67 | done 68 | rm -r "$trycase" 69 | 70 | if [ "$ERRORS" -eq 0 ] 71 | then 72 | printf "\033[32;1m✓ %s PASSED\033[0m\n" "$1" 73 | else 74 | printf "\n❌ \033[31;1m%s FAILED (%d error%s)\033[0m\n" "$1" "$ERRORS" "$(plural "$ERRORS")" 75 | FOUND_ERROR=1 76 | fi 77 | } 78 | 79 | TRY_TOP="${TRY_TOP-$(git rev-parse --show-toplevel --show-superproject-working-tree)}" 80 | 81 | FOUND_ERROR=0 82 | check_file "$TRY_TOP/try" 83 | check_file "$TRY_TOP/utils/try-summary.c" 84 | check_file "$TRY_TOP/utils/try-commit.c" 85 | check_file "$TRY_TOP/test/all-commit-cases.sh" 86 | exit "$FOUND_ERROR" 87 | -------------------------------------------------------------------------------- /scripts/check_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SCRIPT_VERSION="$(grep 'TRY_VERSION=' try | cut -d'"' -f 2)" 4 | MANPAGE_VERSION="$(grep 'TRY(1)' docs/try.1.md | cut -d' ' -f 4)" 5 | INCLUDE_VERSION="$(grep '#define TRY_VERSION' utils/version.h | cut -d'"' -f 2)" 6 | CONFIGAC_VERSION="$(grep AC_INIT configure.ac | cut -d'[' -f3 | cut -d']' -f1)" 7 | 8 | if [ "$SCRIPT_VERSION" = "$MANPAGE_VERSION" ] && 9 | [ "$SCRIPT_VERSION" = "$INCLUDE_VERSION" ] && 10 | [ "$SCRIPT_VERSION" = "$CONFIGAC_VERSION" ] 11 | then 12 | printf "\033[32;1m✓ VERSIONS MATCH (%s) \033[0m\n" "$SCRIPT_VERSION" 13 | else 14 | echo " SCRIPT_VERSION = '$SCRIPT_VERSION'" 15 | echo " MANPAGE_VERSION = '$MANPAGE_VERSION'" 16 | echo " INCLUDE_VERSION = '$INCLUDE_VERSION'" 17 | echo "CONFIGAC_VERSION = '$CONFIGAC_VERSION'" 18 | printf "\n❌ \033[31;1mVERSIONS DO NOT MATCH\033[0m\n" 19 | fi 20 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NUM_WARNINGS=0 4 | 5 | ################################################################################ 6 | # issue a warning 7 | ################################################################################ 8 | 9 | warn() { 10 | msg="$1" 11 | 12 | printf "%s\n" "$msg" >&2 13 | } 14 | 15 | ################################################################################ 16 | # pluralize a word 17 | ################################################################################ 18 | 19 | plural() { 20 | if [ "$1" -eq 1 ] 21 | then 22 | echo "" 23 | else 24 | echo "s" 25 | fi 26 | } 27 | 28 | ################################################################################ 29 | # check for a trailing newline 30 | ################################################################################ 31 | 32 | trailing_newline() { 33 | file="$1" 34 | 35 | [ -f "$file" ] || warn "trailing_newline: '$file' is not a normal file" 36 | 37 | # empty file is fine 38 | [ -s "$file" ] || return 39 | 40 | last="$(tail -c1 "$file")" 41 | if [ "$last" != "$(printf '\n')" ] 42 | then 43 | warn "$file: missing a trailing newline" 44 | fi 45 | } 46 | 47 | ################################################################################ 48 | # check for trailing whitespace on a line 49 | ################################################################################ 50 | 51 | trailing_whitespace() { 52 | file="$1" 53 | 54 | [ -f "$file" ] || warn "trailing_whitespace: '$file' is not a normal file" 55 | 56 | # empty file is fine 57 | [ -s "$file" ] || return 58 | 59 | if grep -I -lq -e '[[:blank:]]$' "$file" 60 | then 61 | warn "$file: trailing whitespace (tabs shown as underscores)" 62 | grep --line-number -e '[[:blank:]]$' "$file" | sed 's/[[:blank:]]\+$/\o33[41m&\o033[0m/' | tr "$(printf '\t')" '____' 63 | fi 64 | } 65 | 66 | ################################################################################ 67 | # check for tabs 68 | ################################################################################ 69 | 70 | tabs() { 71 | file="$1" 72 | 73 | # it's supposed to be there! 74 | case "$file" in 75 | (*Makefile*) return;; 76 | esac 77 | 78 | [ -f "$file" ] || warn "tabs: '$file' is not a normal file" 79 | 80 | # empty file is fine 81 | [ -s "$file" ] || return 82 | 83 | # so it doesn't literally appear here, lol 84 | tab="$(printf '\t')" 85 | if grep -I -lq -e "$tab" "$file" 86 | then 87 | warn "$file: tabs (shown as underscores here)" 88 | grep --line-number -e "$tab" "$file" | sed 's/\t/\o33[41m____\o033[0m/' 89 | fi 90 | } 91 | 92 | ################################################################################ 93 | # entry point 94 | ################################################################################ 95 | 96 | if [ "$#" -eq 0 ] 97 | then 98 | FILES=$(find . -type f -a \( \! -path "./.git/*" \) -a \( \! -path "./.vagrant/*" \) -a \( \! -path "./autom4te.cache/*" \) -a \( \! -name install-sh \) -a \( \! -name "config*" \) -a \( \! -name "*~" \) -a \! \( -name "*.png" -o -name "*.gif" -o -name "*.gz" \)) 99 | else 100 | # shellcheck disable=SC2124 101 | FILES="$@" 102 | fi 103 | 104 | 105 | num_files="$(echo "$FILES" | wc -l)" 106 | printf "Linting %s file%s in parallel..." "$num_files" "$(plural "$num_files")" 107 | OUTPUTS="" 108 | for file in $FILES 109 | do 110 | out=$(mktemp) 111 | printf "." 112 | 113 | ( 114 | trailing_whitespace "$file" 115 | trailing_newline "$file" 116 | tabs "$file" 117 | ) >"$out" 2>&1 & 118 | 119 | OUTPUTS="$OUTPUTS $out" 120 | done 121 | 122 | wait 123 | printf "done.\n" 124 | 125 | for out in $OUTPUTS 126 | do 127 | [ -s "$out" ] && : $((NUM_WARNINGS += 1)) && echo 128 | cat "$out" 129 | rm "$out" 130 | done 131 | 132 | if [ "$NUM_WARNINGS" -eq 0 ] 133 | then 134 | printf "\033[32;1m✓ LINT CHECK PASSED\033[0m\n" 135 | else 136 | printf "\n❌ \033[31;1mLINT CHECK FAILED (%d warning%s)\033[0m\n" "$NUM_WARNINGS" "$(plural "$NUM_WARNINGS")" 137 | exit 1 138 | fi 139 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | : "${TRY_TOP=$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | 5 | # something else to try 6 | if ! [ -d "$TRY_TOP/test" ] 7 | then 8 | TRY_TOP="$PWD" 9 | fi 10 | TEST_DIR="$TRY_TOP/test" 11 | 12 | [ -d "$TEST_DIR" ] || { 13 | echo "Couldn't find test directory (looked in $TEST_DIR)" 14 | exit 2 15 | } 16 | export TRY_TOP 17 | 18 | # set the DEBUG env variable to see detailed output 19 | DEBUG=${DEBUG:-0} 20 | 21 | TOTAL_TESTS=0 22 | PASSED_TESTS=0 23 | FAILING_TESTS="" 24 | SKIPPED_TESTS="" 25 | 26 | if type try-commit >/dev/null 2>&1 27 | then 28 | have_utils() { true; } 29 | else 30 | have_utils() { false; } 31 | fi 32 | 33 | run_test() 34 | { 35 | test="$1" 36 | name="$(basename "$test")" 37 | 38 | if ! [ -f "$test" ] 39 | then 40 | echo "Test '$name' does not exist (at path $test)." 41 | return 1 42 | elif ! [ -x "$test" ] 43 | then 44 | echo "Test '$name' is not executable (at path $test)." 45 | return 1 46 | fi 47 | 48 | # check if we can read from /run dir 49 | ls /run/systemd >/dev/null || { 50 | echo "Cannot read from /run/systemd, aborting test '$name'." >&2 51 | return 1 52 | } 53 | 54 | printf "\nRunning %s...%$((60 - (8 + ${#name} + 3) ))s" "$name" "" 55 | 56 | if grep -q -e "needs-try-utils" "$test" && ! have_utils 57 | then 58 | printf "SKIP (no utils)\n" 59 | SKIPPED_TESTS="$SKIPPED_TESTS $name" 60 | return 61 | fi 62 | 63 | # actually run the test 64 | out=$(mktemp) 65 | err=$(mktemp) 66 | "$test" >"$out" 2>"$err" 67 | ec="$?" 68 | 69 | : $((TOTAL_TESTS += 1)) 70 | if [ "$ec" -eq 0 ]; then 71 | : $((PASSED_TESTS += 1)) 72 | printf "PASS\n" 73 | else 74 | FAILING_TESTS="$FAILING_TESTS $name" 75 | printf "FAIL (%d)\n" "$ec" 76 | 77 | printf "=======\n" 78 | printf "STDOUT:\n" 79 | printf "=======\n" 80 | cat "$out" 81 | printf "=======\n\n" 82 | 83 | printf "=======\n" 84 | printf "STDERR:\n" 85 | printf "=======\n" 86 | cat "$err" 87 | printf "=======\n\n" 88 | fi 89 | 90 | rm "$out" "$err" 91 | } 92 | 93 | pats="$(mktemp)" 94 | if [ "$#" -eq 0 ] 95 | then 96 | # if no patterns are specified 97 | echo '.*' >"$pats" 98 | else 99 | printf "%s\n" "$@" >>"$pats" 100 | fi 101 | 102 | TESTS="$(find "$TEST_DIR" -type f -executable -name '*.sh' | grep -f "$pats")" 103 | rm "$pats" 104 | 105 | echo "========================| Try Tests |===========================" 106 | echo "Running $(echo "$TESTS" | wc -l) tests..." 107 | echo "================================================================" 108 | 109 | for test in $TESTS 110 | do 111 | run_test "$test" 112 | done 113 | 114 | echo 115 | echo "========================| Test Summary |========================" 116 | echo "Failing tests:${FAILING_TESTS}" 117 | echo "Skipped tests:${SKIPPED_TESTS}" 118 | echo "Summary: ${PASSED_TESTS}/${TOTAL_TESTS} tests passed." 119 | echo "================================================================" 120 | 121 | if [ "$PASSED_TESTS" -ne "$TOTAL_TESTS" ] 122 | then 123 | exit 1 124 | fi 125 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}}: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ 5 | expect 6 | mergerfs 7 | attr 8 | util-linux 9 | time 10 | shellcheck 11 | autoconf 12 | pandoc 13 | (pkgs.callPackage ./package.nix {}) 14 | ]; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | This directory holds tests and testing resources. 2 | 3 | `scripts/run_tests.sh` will run all of the tests in this directory 4 | (and subdirectories); to be a test, a file must be: 5 | 6 | - a regular file 7 | - executable 8 | - end in `.sh` 9 | 10 | You can select a specific test by running `scripts/run_tests.sh a b`; 11 | this will run tests that pass `a` _or_ `b` as grep patterns. 12 | 13 | Some files are named `.sh.skip`---these will always be skipped. Rename 14 | them if you need to run them. 15 | 16 | Tests have a pretty standard preamble that should handle basic cleanup 17 | functions. 18 | -------------------------------------------------------------------------------- /test/all-commit-cases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck disable=SC2119 # warnings about fail's arguments (put before first command to be global) 4 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 5 | TRY="$TRY_TOP/try" 6 | 7 | cleanup() { 8 | cd / 9 | 10 | if [ -d "$try_workspace" ] 11 | then 12 | rm -rf "$try_workspace" >/dev/null 2>&1 13 | fi 14 | } 15 | 16 | trap 'cleanup' EXIT 17 | 18 | try_workspace="$(mktemp -d)" 19 | cd "$try_workspace" || exit 99 20 | 21 | # we have a case for every TRYCASE(changed_file, local_file) in the utils 22 | # - first arg is incoming change 23 | # - second arg is local file status 24 | # see utils/README.md for more info 25 | 26 | COUNT=0 27 | fail() { 28 | echo Case $COUNT: "$@" 29 | exit ${COUNT} 30 | } 31 | 32 | 33 | # // TRYCASE(opaque, dir) 34 | # // TRYCASE(dir, dir) 35 | 36 | : $((COUNT += 1)) 37 | 38 | ! [ -e olddir ] || fail 39 | mkdir olddir || fail 40 | echo hi >olddir/oldfile || fail 41 | [ -d olddir ] || fail 42 | [ -f olddir/oldfile ] || fail 43 | "$TRY" -y "rm -r olddir; mkdir olddir; echo fresh >olddir/newfile" || fail 44 | [ -d olddir ] || fail 45 | [ -f olddir/newfile ] || fail 46 | ! [ -e olddir/oldfile ] || fail opaque directories worked incorrectly 47 | [ "$(cat olddir/newfile)" = "fresh" ] || fail 48 | rm -r olddir || fail 49 | 50 | # // TRYCASE(dir, nonexist) 51 | 52 | : $((COUNT += 1)) 53 | 54 | ! [ -e newdir ] || fail 55 | "$TRY" -y "mkdir newdir" || fail 56 | [ -d newdir ] || fail 57 | rmdir newdir || fail 58 | 59 | # // TRYCASE(opaque, file) 60 | # // TRYCASE(dir, file) 61 | 62 | : $((COUNT += 1)) 63 | 64 | ! [ -e wasfile ] || fail 65 | echo hi >wasfile || fail 66 | [ -f wasfile ] || fail 67 | "$TRY" -y "mv wasfile stillfile; mkdir wasfile; mv stillfile wasfile" || fail 68 | [ -d wasfile ] || fail 69 | [ -f wasfile/stillfile ] || fail 70 | [ "$(cat wasfile/stillfile)" = "hi" ] || fail 71 | rm -r wasfile || fail 72 | 73 | # // TRYCASE(opaque, symlink) 74 | # // TRYCASE(dir, symlink) 75 | 76 | : $((COUNT += 1)) 77 | 78 | ! [ -e waslink ] || fail 79 | ln -s "$TRY" waslink || fail 80 | [ -L waslink ] || fail 81 | "$TRY" -y "rm waslink; mkdir waslink; ln -s \"$TRY\" waslink/newlink" || fail 82 | [ -d waslink ] || fail 83 | [ -L waslink/newlink ] || fail 84 | [ "$(readlink waslink/newlink)" = "$TRY" ] || fail 85 | rm -r waslink || fail 86 | 87 | # // TRYCASE(whiteout, dir) 88 | 89 | : $((COUNT += 1)) 90 | 91 | mkdir existdir || fail 92 | touch existdir/file || fail 93 | [ -d existdir ] || fail 94 | "$TRY" -y "rm -r existdir" || fail 95 | ! [ -d existdir ] || fail 96 | 97 | # // TRYCASE(whiteout, file) 98 | 99 | : $((COUNT += 1)) 100 | 101 | ! [ -e goner ] || fail 102 | echo alas >goner || fail 103 | [ -f goner ] || fail 104 | "$TRY" -y "rm goner" || fail 105 | ! [ -e goner ] || fail 106 | 107 | # // TRYCASE(whiteout, symlink) 108 | 109 | : $((COUNT += 1)) 110 | 111 | ! [ -e trylink ] || fail 112 | ln -s "$TRY" trylink || fail 113 | [ -L trylink ] || fail 114 | "$TRY" -y "rm trylink" || fail 115 | ! [ -e trylink ] || fail 116 | 117 | # // TRYCASE(whiteout, nonexist) 118 | # this case is not really possible, but the following _could_ exercise it 119 | 120 | : $((COUNT += 1)) 121 | 122 | ! [ -e absent ] || fail 123 | "$TRY" -y "touch absent; sync; rm absent" || fail 124 | ! [ -e absent ] || fail 125 | 126 | # // TRYCASE(file, symlink) 127 | 128 | : $((COUNT += 1)) 129 | 130 | ! [ -e sneaky ] || fail 131 | ln -s "$TRY" sneaky || fail 132 | [ -L sneaky ] || fail 133 | "$TRY" -y "rm sneaky; echo gotcha >sneaky" || fail 134 | [ -f sneaky ] || fail 135 | [ "$(cat sneaky)" = "gotcha" ] || fail 136 | rm sneaky || fail 137 | 138 | # // TRYCASE(file, nonexist) 139 | 140 | : $((COUNT += 1)) 141 | 142 | ! [ -e missingno ] || fail 143 | "$TRY" -y "echo arceus >missingno" || fail 144 | [ -f missingno ] || fail 145 | [ "$(cat missingno)" = "arceus" ] || fail 146 | rm missingno || fail 147 | 148 | # // TRYCASE(file, dir) 149 | 150 | : $((COUNT += 1)) 151 | 152 | ! [ -e wasdir ] || fail 153 | mkdir wasdir || fail 154 | mkdir wasdir/inner || fail 155 | touch wasdir/inner/goodbye || fail 156 | [ -d wasdir ] || fail 157 | [ -d wasdir/inner ] || fail 158 | [ -f wasdir/inner/goodbye ] || fail 159 | "$TRY" -y "rm -r wasdir; echo a humble file >wasdir" || fail 160 | [ -f wasdir ] || fail 161 | [ "$(cat wasdir)" = "a humble file" ] || fail 162 | 163 | # // TRYCASE(file, file) 164 | 165 | : $((COUNT += 1)) 166 | 167 | ! [ -e wasfile ] || fail 168 | echo sup >wasfile || fail 169 | [ -f wasfile ] || fail 170 | "$TRY" -y "echo what is up >wasfile" || fail 171 | [ -f wasfile ] || fail 172 | [ "$(cat wasfile)" = "what is up" ] || fail 173 | 174 | # // TRYCASE(symlink, nonexist) 175 | 176 | : $((COUNT += 1)) 177 | 178 | ! [ -e linktobe ] || fail 179 | "$TRY" -y "ln -s \"$TRY\" linktobe" || fail 180 | [ -L linktobe ] || fail 181 | [ "$(readlink linktobe)" = "$TRY" ] || fail 182 | 183 | # // TRYCASE(symlink, file) 184 | 185 | : $((COUNT += 1)) 186 | 187 | ! [ -e gonnago ] || fail 188 | echo so long >gonnago || fail 189 | "$TRY" -y "ln -sf \"$TRY\" gonnago" || fail 190 | [ -L gonnago ] || fail 191 | [ "$(readlink gonnago)" = "$TRY" ] || fail 192 | rm gonnago || fail 193 | 194 | # // TRYCASE(symlink, symlink) 195 | 196 | : $((COUNT += 1)) 197 | 198 | ! [ -e otherlink ] || fail 199 | ln -s /etc/passwd otherlink || fail 200 | "$TRY" -y "ln -sf \"$TRY\" otherlink" || fail 201 | [ -L otherlink ] || fail 202 | [ "$(readlink otherlink)" = "$TRY" ] || fail 203 | rm otherlink || fail 204 | 205 | # // TRYCASE(symlink, dir) 206 | 207 | : $((COUNT += 1)) 208 | 209 | ! [ -e formerdir ] || fail 210 | mkdir formerdir || fail 211 | mkdir formerdir/en formerdir/it || fail 212 | echo hello >formerdir/en/file1 || fail 213 | echo goodbye >formerdir/en/file2 || fail 214 | echo buuongiorno >formerdir/it/file1 || fail 215 | echo arrivederci >formerdir/it/file2 || fail 216 | [ -d formerdir ] || fail 217 | [ -d formerdir/en ] || fail 218 | [ -d formerdir/it ] || fail 219 | [ -f formerdir/en/file1 ] || fail 220 | [ -f formerdir/en/file2 ] || fail 221 | [ -f formerdir/it/file1 ] || fail 222 | [ -f formerdir/it/file2 ] || fail 223 | "$TRY" -y "rm -r formerdir; ln -s /this/is/a/broken/symlink formerdir" || fail 224 | [ -L formerdir ] || fail 225 | [ "$(readlink formerdir)" = "/this/is/a/broken/symlink" ] || fail 226 | rm formerdir || fail 227 | -------------------------------------------------------------------------------- /test/command_substitution.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$expected" ] 15 | then 16 | rm "$expected" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cd "$try_workspace" || exit 9 24 | 25 | # Set up expected output 26 | expected="$(mktemp)" 27 | pwd >"$expected" 28 | 29 | cat >script.sh <<"EOF" 30 | mypwd="$(pwd)" 31 | echo "$mypwd" 32 | EOF 33 | 34 | "$TRY" sh script.sh >out.txt || exit 1 35 | 36 | diff -q "$expected" out.txt 37 | -------------------------------------------------------------------------------- /test/dev_urandom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | # Ignore changes to foo 21 | "$TRY" -y "head -c 5 /dev/urandom >target" || exit 1 22 | [ -s target ] || exit 2 23 | -------------------------------------------------------------------------------- /test/empty_summary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cd "$try_workspace" || exit 9 24 | 25 | try_example_dir="$(mktemp -d)" 26 | "$TRY" -D "$try_example_dir" -- echo hi >/dev/null || exit 1 27 | "$TRY" summary "$try_example_dir" >summary.out 28 | 29 | # an empty summary yields exit status 1 30 | [ "$?" -eq 1 ] || exit 2 31 | 32 | # We want ec 0 if the following line is not found! 33 | ! grep -q -e "Changes detected in the following files:" summary.out 34 | -------------------------------------------------------------------------------- /test/exit_status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | "$TRY" exit 3 21 | 22 | [ "$?" -eq 3 ] 23 | -------------------------------------------------------------------------------- /test/explore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | SHELL="/usr/bin/env bash --norc" 21 | export SHELL 22 | PS1="# " 23 | export PS1 24 | 25 | echo hi >expected.out 26 | 27 | cat >explore.exp <test.txt\r" 41 | } 42 | } 43 | expect "#" 44 | # Send exit 45 | send \x04 46 | 47 | # Ignore all output and just send a y at the end 48 | expect "" 49 | expect "Commit*" 50 | send -- "y\r" 51 | expect eof 52 | EOF 53 | 54 | # Debug using the -d flag 55 | expect explore.exp >/dev/null || exit 1 56 | 57 | diff -q expected.out test.txt 58 | -------------------------------------------------------------------------------- /test/fail.sh.skip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # rename this to fail.sh to force a CI failure (e.g., when testing CI changes) 4 | 5 | exit 1 6 | -------------------------------------------------------------------------------- /test/hidden_variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | # Set up expected output 21 | echo 'no sandbox' >expected.out 22 | 23 | # SANDBOX_DIR should not be set in the final execution env 24 | "$TRY" -y -- "echo \${SANDBOX_DIR-no sandbox}" >got.out 2>/dev/null || exit 1 25 | 26 | diff -q expected.out got.out 27 | -------------------------------------------------------------------------------- /test/ignore_flag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | # Set up expected output 21 | touch expected.bar 22 | 23 | # Ignore changes to foo 24 | "$TRY" -y -i foo1 -i foo2 "touch foo1.txt; touch foo2.txt; touch bar.txt" || exit 1 25 | 26 | diff -q expected.bar bar.txt || exit 2 27 | ! [ -f foo1.txt ] || exit 3 28 | ! [ -f foo2.txt ] || exit 4 29 | -------------------------------------------------------------------------------- /test/merge_multiple_dirs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$try_example_dir1" ] 15 | then 16 | rm "$try_example_dir1" 17 | fi 18 | 19 | if [ -f "$try_example_dir2" ] 20 | then 21 | rm "$try_example_dir2" 22 | fi 23 | 24 | if [ -f "$try_example_dir3" ] 25 | then 26 | rm "$try_example_dir3" 27 | fi 28 | 29 | if [ -f "$expected1" ] 30 | then 31 | rm "$expected1" 32 | fi 33 | 34 | if [ -f "$expected2" ] 35 | then 36 | rm "$expected2" 37 | fi 38 | if [ -f "$expected3" ] 39 | then 40 | rm "$expected3" 41 | fi 42 | } 43 | 44 | trap 'cleanup' EXIT 45 | 46 | # (ezri) gh ci gid fix 47 | cd ~ || exit 1 48 | 49 | try_workspace="$(mktemp -d -p .)" 50 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 51 | cd "$try_workspace" || exit 1 52 | 53 | try_example_dir1="$(mktemp -d)" 54 | try_example_dir2="$(mktemp -d)" 55 | try_example_dir3="$(mktemp -d)" 56 | 57 | expected1="$(mktemp)" 58 | expected2="$(mktemp)" 59 | expected3="$(mktemp)" 60 | 61 | touch "$expected1" 62 | echo "test2" > "$expected2" 63 | echo "test3" > "$expected3" 64 | 65 | "$TRY" -D "$try_example_dir1" "touch file_1.txt; echo test > file_2.txt; rm file.txt.gz" || exit 2 66 | "$TRY" -D "$try_example_dir2" "echo test2 > file_2.txt" || exit 3 67 | "$TRY" -D "$try_example_dir3" "echo test3 > file_3.txt" || exit 4 68 | "$TRY" -L "$try_example_dir3:$try_example_dir2:$try_example_dir1" -y "cat file_1.txt > out1; cat file_2.txt > out2; cat file_3.txt > out3"|| exit 5 69 | 70 | diff -q "$expected1" out1 || exit 6 71 | diff -q "$expected2" out2 || exit 7 72 | diff -q "$expected3" out3 || exit 8 73 | 74 | ! [ -f out4 ] || exit 9 75 | -------------------------------------------------------------------------------- /test/missing_unionfs_mergerfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # checking that try works when mergerfs/unionfs are not present (but also not necessary) 4 | 5 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 6 | TRY="$TRY_TOP/try" 7 | 8 | cleanup() { 9 | cd / 10 | 11 | if [ -d "$try_workspace" ] 12 | then 13 | rm -rf "$try_workspace" >/dev/null 2>&1 14 | fi 15 | 16 | if [ -d "$new_bin_dir" ] 17 | then 18 | rm -rf "$new_bin_dir" 19 | fi 20 | } 21 | 22 | trap 'cleanup' EXIT 23 | 24 | run_regular() { 25 | new_bin_dir="$(mktemp -d)" 26 | mkdir "$new_bin_dir/usr" 27 | # -s makes symlinks 28 | cp -rs /usr/bin "$new_bin_dir/usr/bin" 29 | 30 | # Delete mergerfs and unionfs and set the new PATH to the temporary directory 31 | rm -f "$new_bin_dir/usr/bin/mergerfs" 2>/dev/null 32 | rm -f "$new_bin_dir/usr/bin/unionfs" 2>/dev/null 33 | 34 | echo hi >expected 35 | PATH="$new_bin_dir/usr/bin" "$TRY" -y "echo hi" >target 2>/dev/null || exit 1 36 | diff -q expected target || exit 2 37 | } 38 | 39 | run_nix() { 40 | cat > shell.nix <<'EOF' 41 | { pkgs ? import {} }: 42 | pkgs.mkShell { 43 | buildInputs = with pkgs; [ 44 | attr 45 | ]; 46 | } 47 | EOF 48 | 49 | echo hi >expected 50 | # Run the command in a nix-shell with only the specified packages 51 | nix-shell --pure shell.nix --run "\"$TRY\" -y \"echo hi\"" >target 2>/dev/null || exit 3 52 | diff -q expected target || exit 4 53 | } 54 | 55 | # particularly important that we run in mktemp: in some test machines, 56 | # the cwd is mounted, hence inaccessable. 57 | try_workspace="$(mktemp -d)" 58 | cd "$try_workspace" || exit 9 59 | 60 | if [ -e /etc/NIXOS ] 61 | then 62 | run_nix 63 | else 64 | run_regular 65 | fi 66 | -------------------------------------------------------------------------------- /test/mkdir_on_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | # Set up expected output 21 | touch target 22 | mkdir expected 23 | 24 | "$TRY" -y "rm target; mkdir target" || exit 1 25 | diff -qr expected target 26 | -------------------------------------------------------------------------------- /test/network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | # Test if network works normally 7 | # using curl due to #131 (1.1 expands to 1.0.0.1) 8 | "$TRY" curl 1.1 || exit 1 9 | 10 | # Test if curl fails when network is unshared 11 | # curl exit code 7 means Failed to connect to host. 12 | "$TRY" -x curl 1.1 13 | if [ $? -eq 7 ] 14 | then 15 | exit 0 16 | else 17 | exit 1 18 | fi 19 | -------------------------------------------------------------------------------- /test/newline-filename.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # needs-try-utils 4 | 5 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 6 | TRY="$TRY_TOP/try" 7 | 8 | cleanup() { 9 | cd / 10 | 11 | if [ -d "$try_workspace" ] 12 | then 13 | rm -rf "$try_workspace" >/dev/null 2>&1 14 | fi 15 | } 16 | 17 | trap 'cleanup' EXIT 18 | 19 | try_workspace="$(mktemp -d)" 20 | cd "$try_workspace" || exit 9 21 | 22 | badname="$(printf "this\\nsucks")" 23 | touch "$badname" || exit 1 24 | [ -f "$badname" ] || exit 2 25 | # shellcheck disable=SC2016 26 | "$TRY" -y 'rm "$(printf "this\nsucks")"' || exit 3 27 | ! [ -f "$badname" ] || exit 4 28 | -------------------------------------------------------------------------------- /test/nonexistent_sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | # shouldn't fire 15 | if [ -d "$try_example_dir" ] 16 | then 17 | rm -rf "$try_example_dir" 18 | fi 19 | } 20 | 21 | trap 'cleanup' EXIT 22 | 23 | try_workspace="$(mktemp -d)" 24 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 25 | cd "$try_workspace" || exit 9 26 | 27 | try_example_dir="non-existent" 28 | ! [ -d "$try_example_dir" ] || exit 2 29 | ! "$TRY" -D $try_example_dir "touch file_1.txt" 2>/dev/null && 30 | ! "$TRY" summary $try_example_dir 2>/dev/null && 31 | ! "$TRY" commit $try_example_dir 2>/dev/null && 32 | ! "$TRY" explore $try_example_dir 2>/dev/null 33 | -------------------------------------------------------------------------------- /test/pipeline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$expected" ] 15 | then 16 | rm "$expected" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 24 | cd "$try_workspace" || exit 9 25 | 26 | # Set up expected output 27 | expected="$(mktemp)" 28 | echo 'TesT' >"$expected" 29 | 30 | "$TRY" 'echo test | tr t T' >out.txt || exit 1 31 | 32 | diff -q "$expected" out.txt 33 | -------------------------------------------------------------------------------- /test/resources/file.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binpash/try/351896f51409423aeaa93f6d1fed98614035751f/test/resources/file.txt.gz -------------------------------------------------------------------------------- /test/reuse_problematic_sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 24 | cd "$try_workspace" || exit 9 25 | 26 | try_example_dir=$(mktemp -d) 27 | "$TRY" -D "$try_example_dir" "touch file_1.txt; echo test >file_2.txt; rm file.txt.gz" || exit 1 28 | 29 | # KK 2023-06-29 This test is meant to modify the sandbox directory in an illegal way, 30 | # at the moment, this modification will be caught as illegal by `try`, 31 | # but it doesn't seem to both overlayfs at all. 32 | # TODO: Extend this with more problematic overlayfs modifications. 33 | touch "$try_example_dir/temproot/etc/foo" 34 | ! "$TRY" -D "$try_example_dir" "rm file_1.txt; echo test2 >>file_2.txt; touch file.txt.gz" 2>/dev/null 35 | -------------------------------------------------------------------------------- /test/reuse_sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | 19 | if [ -f "$expected1" ] 20 | then 21 | rm "$expected1" 22 | fi 23 | 24 | if [ -f "$expected2" ] 25 | then 26 | rm "$expected2" 27 | fi 28 | } 29 | 30 | trap 'cleanup' EXIT 31 | 32 | try_workspace="$(mktemp -d)" 33 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 34 | cd "$try_workspace" || exit 9 35 | 36 | expected1="$(mktemp)" 37 | expected2="$(mktemp)" 38 | echo test >"$expected1" 39 | echo test2 >>"$expected1" 40 | touch "$expected2" 41 | 42 | try_example_dir="$(mktemp -d)" 43 | "$TRY" -D "$try_example_dir" "touch file_1.txt; echo test >file_2.txt; rm file.txt.gz" || exit 1 44 | "$TRY" -D "$try_example_dir" "rm file_1.txt; echo test2 >>file_2.txt; touch file.txt.gz" || exit 2 45 | "$TRY" commit "$try_example_dir" || exit 3 46 | 47 | ! [ -f file_1.txt ] || exit 4 48 | diff -q "$expected1" file_2.txt || exit 5 49 | diff -q "$expected2" file.txt.gz || exit 6 50 | -------------------------------------------------------------------------------- /test/stdstream.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cmdfile="$(mktemp)" 7 | 8 | cat > "$cmdfile" <<'EOF' 9 | read x < /dev/stdin 10 | echo $((x * 2)) > /dev/stdout 11 | echo $((x * 3)) > /dev/stderr 12 | 13 | EOF 14 | 15 | chmod +x "$cmdfile" 16 | 17 | try_stdout=$(mktemp) 18 | try_stderr=$(mktemp) 19 | sh_stdout=$(mktemp) 20 | sh_stderr=$(mktemp) 21 | 22 | # test stdout 23 | echo 5 | "$TRY" "$cmdfile" >"$try_stdout" 2>"$try_stderr" 24 | echo 5 | sh "$cmdfile" >"$sh_stdout" 2>"$sh_stderr" 25 | 26 | diff "$try_stdout" "$sh_stdout" || exit 1 27 | 28 | # using grep because there's try errors printed 29 | grep -q 15 "$try_stderr" 30 | grep -q 15 "$sh_stderr" 31 | 32 | rm "$try_stdout" "$try_stderr" "$sh_stdout" "$sh_stderr" 33 | 34 | cat > "$cmdfile" <<'EOF' 35 | read x <&0 36 | echo $((x * 2)) >&1 37 | echo $((x * 3)) >&2 38 | 39 | EOF 40 | 41 | # test stdout 42 | echo 5 | "$TRY" "$cmdfile" >"$try_stdout" 2>"$try_stderr" 43 | echo 5 | sh "$cmdfile" >"$sh_stdout" 2>"$sh_stderr" 44 | 45 | diff "$try_stdout" "$sh_stdout" || exit 1 46 | 47 | # using grep because there's try errors printed 48 | grep -q 15 "$try_stderr" 49 | grep -q 15 "$sh_stderr" 50 | 51 | rm "$try_stdout" "$try_stderr" "$sh_stdout" "$sh_stderr" 52 | -------------------------------------------------------------------------------- /test/summary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cd "$try_workspace" || exit 9 24 | 25 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 26 | 27 | # Set up expected output 28 | touch expected1.txt 29 | echo 'test' >expected2.txt 30 | 31 | echo 'fail' >file_2.txt 32 | touch target 33 | 34 | try_example_dir=$(mktemp -d) 35 | "$TRY" -D "$try_example_dir" "touch file_1.txt; echo test >file_2.txt; rm file.txt.gz; rm target; mkdir target; mkdir new_dir" || exit 1 36 | "$TRY" summary "$try_example_dir" >summary.out || exit 2 37 | 38 | # Check that the summary correctly identifies every change 39 | grep -qx -e "$PWD/file_1.txt (added)" summary.out || exit 3 40 | grep -qx -e "$PWD/file_2.txt (modified)" summary.out || exit 4 41 | grep -qx -e "$PWD/file.txt.gz (deleted)" summary.out || exit 5 42 | grep -qx -e "$PWD/target (replaced with dir)" summary.out || exit 6 43 | grep -qx -e "$PWD/new_dir (created dir)" summary.out || exit 7 44 | -------------------------------------------------------------------------------- /test/symlinks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | } 14 | 15 | trap 'cleanup' EXIT 16 | 17 | try_workspace="$(mktemp -d)" 18 | cd "$try_workspace" || exit 9 19 | 20 | "$TRY" -y ln -s foo bar 21 | 22 | result=$(readlink bar) 23 | expected="foo" 24 | 25 | [ "$result" = "$expected" ] || exit 1 26 | -------------------------------------------------------------------------------- /test/tempfiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC2010,SC2126,SC2181 3 | 4 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 5 | TRY="$TRY_TOP/try" 6 | 7 | workdir="$(mktemp -d)" 8 | cd "$workdir" || exit 1 9 | 10 | initial_count="$(ls "${TMPDIR-/tmp}" | grep -e "^.*\.try-[0-9]*$" | wc -l)" 11 | 12 | sandbox=$($TRY -n "touch $HOME/foo") 13 | [ $? -eq 0 ] || exit 2 14 | 15 | post_count="$(ls "${TMPDIR-/tmp}" | grep -e "^.*\.try-[0-9]*$" | wc -l)" 16 | 17 | # just one new tempfile 18 | [ "$((initial_count + 1))" -eq "$post_count" ] || exit 3 19 | [ -f "$sandbox/upperdir$HOME/foo" ] || exit 4 20 | 21 | # deliberately not the pattern of try sandboxes 22 | sandbox=local 23 | mkdir "$sandbox" || exit 5 24 | $TRY -D "$sandbox" "touch $HOME/bar" || exit 6 25 | 26 | final_count="$(ls "${TMPDIR-/tmp}" | grep -e "^.*\.try-[0-9]*$" | wc -l)" 27 | 28 | # no new tempfiles! 29 | [ "$post_count" -eq "$final_count" ] || exit 7 30 | [ -f "$sandbox/upperdir$HOME/bar" ] || exit 8 31 | -------------------------------------------------------------------------------- /test/toplevel-perms.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$expected" ] 15 | then 16 | rm "$expected" 17 | fi 18 | 19 | if [ -f "$target" ] 20 | then 21 | rm "$target" 22 | fi 23 | 24 | if [ -f "$cmd" ] 25 | then 26 | rm "$cmd" 27 | fi 28 | } 29 | 30 | trap 'cleanup' EXIT 31 | 32 | try_workspace="$(mktemp -d)" 33 | cd "$try_workspace" || exit 9 34 | touch test 35 | 36 | cmd="$(mktemp)" 37 | echo "find / -maxdepth 1 -print0 | xargs -0 ls -ld | awk '{print substr(\$1, 1, 10), \$9, \$10, \$11}' | grep -v 'proc' | grep -v 'swap' | grep -v 'vmlinuz' | grep -v 'initrd'" > "$cmd" 38 | # Use this after gidmapper to show user and group ownership 39 | #echo "find / -maxdepth 1 -print0 | xargs -0 ls -ld | awk '{print substr(\$1, 1, 10), \$3, \$4, \$9, \$10, \$11}' | grep -v 'proc' | grep -v 'swap'" > "$cmd" 40 | 41 | # Set up expected output 42 | expected="$(mktemp)" 43 | sh "$cmd" >"$expected" 44 | 45 | # Set up target output 46 | target="$(mktemp)" 47 | 48 | "$TRY" "sh $cmd" > "$target" || exit 1 49 | #diff -q "$expected" "$target" 50 | diff "$expected" "$target" 51 | -------------------------------------------------------------------------------- /test/touch_and_rm_D_flag_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | 19 | if [ -f "$expected1" ] 20 | then 21 | rm "$expected1" 22 | fi 23 | 24 | if [ -f "$expected2" ] 25 | then 26 | rm "$expected2" 27 | fi 28 | } 29 | 30 | trap 'cleanup' EXIT 31 | 32 | try_workspace="$(mktemp -d)" 33 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 34 | cd "$try_workspace" || exit 9 35 | 36 | expected1="$(mktemp)" 37 | expected2="$(mktemp)" 38 | touch "$expected1" 39 | echo 'test' >"$expected2" 40 | 41 | try_example_dir="$(mktemp -d)" 42 | "$TRY" -D "$try_example_dir" "touch file_1.txt; echo test >file_2.txt; rm file.txt.gz" || exit 1 43 | "$TRY" commit "$try_example_dir" || exit 2 44 | 45 | diff -q "$expected1" file_1.txt || exit 3 46 | diff -q "$expected2" file_2.txt || exit 4 47 | ! [ -f file.txt.gz ] || exit 5 48 | -------------------------------------------------------------------------------- /test/touch_and_rm_no_flag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$expected1" ] 15 | then 16 | rm "$expected1" 17 | fi 18 | 19 | if [ -f "$expected2" ] 20 | then 21 | rm "$expected2" 22 | fi 23 | } 24 | 25 | trap 'cleanup' EXIT 26 | 27 | try_workspace="$(mktemp -d)" 28 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 29 | cd "$try_workspace" || exit 9 30 | 31 | expected1="$(mktemp)" 32 | expected2="$(mktemp)" 33 | touch "$expected1" 34 | echo 'test' >"$expected2" 35 | 36 | "$TRY" -y "touch file_1.txt; echo test > file_2.txt; rm file.txt.gz" || exit 1 37 | 38 | diff -q "$expected1" file_1.txt || exit 2 39 | diff -q "$expected2" file_2.txt || exit 3 40 | ! [ -f file.txt.gz ] || exit 4 41 | -------------------------------------------------------------------------------- /test/touch_and_rm_with_cleanup.sh.skip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP=${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree)} 4 | TRY="$TRY_TOP/try" 5 | RESOURCE_DIR="$TRY_TOP/test/resources" 6 | 7 | cleanup() { 8 | if [ -d "$try_workspace" ] 9 | then 10 | rm -rf "$try_workspace" 11 | fi 12 | 13 | if [ -d "$try_example_dir" ] 14 | then 15 | rm -rf "$try_example_dir" 16 | fi 17 | } 18 | 19 | trap 'cleanup' EXIT 20 | 21 | try_workspace="$(mktemp -d)" 22 | 23 | cp $RESOURCE_DIR/file.txt.gz "$try_workspace/" 24 | cd "$try_workspace/" 25 | 26 | : ${TMPDIR=/tmp} 27 | 28 | orig_tmp=$(ls "$TMPDIR") 29 | "$try" -y -- "touch file_1.txt; echo test > file_2.txt; rm file.txt.gz" || return 1 30 | new_tmp=$(ls "$TMPDIR") 31 | 32 | if ! diff -q <(echo "$orig_tmp") <(echo "$new_tmp") 33 | then 34 | echo "temporary directory was not cleaned up; diff:" 35 | diff --color -u <(echo "$orig_tmp") <(echo "$new_tmp") 36 | return 1 37 | fi 38 | -------------------------------------------------------------------------------- /test/try-commit-c.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # needs-try-utils 4 | 5 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 6 | TRY="$TRY_TOP/try" 7 | 8 | cleanup() { 9 | cd / 10 | 11 | if [ -d "$try_workspace" ] 12 | then 13 | rm -rf "$try_workspace" >/dev/null 2>&1 14 | fi 15 | } 16 | 17 | trap 'cleanup' EXIT 18 | 19 | try_workspace="$(mktemp -d)" 20 | cd "$try_workspace" || exit 99 21 | 22 | ! [ -e f ] || exit 1 23 | sandbox=$("$TRY" -n "echo hi>f") 24 | # shellcheck disable=SC2181 25 | [ "$?" = 0 ] || exit 2 26 | [ -d "$sandbox" ] || exit 3 27 | [ -d "$sandbox/upperdir" ] || exit 4 28 | [ -f "$sandbox/upperdir/$try_workspace/f" ] || exit 5 29 | [ "$(cat "$sandbox/upperdir/$try_workspace/f")" = "hi" ] || exit 6 30 | try-summary "$sandbox" | grep -q "$try_workspace/f (added)" || exit 7 31 | ! [ -e f ] || exit 8 32 | try-commit -c "$sandbox" || exit 9 33 | [ -f f ] || exit 10 34 | [ "$(cat f)" = "hi" ] || exit 11 35 | [ -f "$sandbox/upperdir/$try_workspace/f" ] || exit 12 36 | [ "$(cat "$sandbox/upperdir/$try_workspace/f")" = "hi" ] || exit 13 37 | -------------------------------------------------------------------------------- /test/unzip_D_flag_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | 19 | if [ -f "$expected" ] 20 | then 21 | rm "$expected" 22 | fi 23 | } 24 | 25 | trap 'cleanup' EXIT 26 | 27 | try_workspace=$(mktemp -d) 28 | cd "$try_workspace" || exit 9 29 | 30 | # Set up expected output 31 | expected="$(mktemp)" 32 | echo 'Hello World!' >"$expected" 33 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 34 | 35 | try_example_dir="$(mktemp -d)" 36 | "$TRY" -D "$try_example_dir" gunzip file.txt.gz || exit 1 37 | "$TRY" commit "$try_example_dir" || exit 2 38 | diff -q "$expected" file.txt || exit 3 39 | -------------------------------------------------------------------------------- /test/unzip_D_flag_commit_without_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -d "$try_example_dir" ] 15 | then 16 | rm -rf "$try_example_dir" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cd "$try_workspace" || exit 9 24 | 25 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 26 | try_example_dir="$(mktemp -d)" 27 | 28 | "$TRY" -D "$try_example_dir" gunzip file.txt.gz || exit 1 29 | if ! [ -d "$try_example_dir" ] 30 | then 31 | echo "try_example_dir disappeared with no commit" 32 | exit 2 33 | fi 34 | "$TRY" commit "$try_example_dir" || exit 3 35 | if ! [ -d "$try_example_dir" ] 36 | then 37 | echo "try_example_dir disappeared after manual commit" 38 | exit 4 39 | fi 40 | -------------------------------------------------------------------------------- /test/unzip_no_flag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TRY_TOP="${TRY_TOP:-$(git rev-parse --show-toplevel --show-superproject-working-tree 2>/dev/null || echo "${0%/*}")}" 4 | TRY="$TRY_TOP/try" 5 | 6 | cleanup() { 7 | cd / 8 | 9 | if [ -d "$try_workspace" ] 10 | then 11 | rm -rf "$try_workspace" >/dev/null 2>&1 12 | fi 13 | 14 | if [ -f "$expected" ] 15 | then 16 | rm "$expected" 17 | fi 18 | } 19 | 20 | trap 'cleanup' EXIT 21 | 22 | try_workspace="$(mktemp -d)" 23 | cd "$try_workspace" || exit 9 24 | 25 | # Set up expected output 26 | expected="$(mktemp)" 27 | echo 'Hello World!' >"$expected" 28 | 29 | cp "$TRY_TOP/test/resources/file.txt.gz" "$try_workspace/" 30 | 31 | "$TRY" -y gunzip file.txt.gz || exit 1 32 | diff -q "$expected" file.txt 33 | -------------------------------------------------------------------------------- /try: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2023 The PaSh Authors. 4 | # 5 | # Usage of this source code is governed by the MIT license, you can find the 6 | # LICENSE file in the root directory of this project. 7 | # 8 | # https://github.com/binpash/try 9 | 10 | TRY_VERSION="0.2.0" 11 | TRY_COMMAND="${0##*/}" 12 | EXECID="$(date +%s%3N)" 13 | export EXECID 14 | export TRY_COMMAND 15 | 16 | # exit status invariants 17 | # 18 | # 0 -- command ran 19 | # 1 -- consistency error/failure 20 | # 2 -- input error 21 | 22 | ################################################################################ 23 | # Run a command (in `$@`) in an overlay (in `$SANDBOX_DIR`) 24 | ################################################################################ 25 | 26 | try() { 27 | START_DIR="$PWD" 28 | 29 | if [ "$SANDBOX_DIR" ] 30 | then 31 | ## If the name of a sandbox is given then we need to exit prematurely if its directory doesn't exist 32 | [ -d "$SANDBOX_DIR" ] || error "could not find sandbox directory $SANDBOX_DIR" 2 33 | # Force absolute path 34 | SANDBOX_DIR="$(cd "$SANDBOX_DIR" && pwd)" 35 | 36 | # shellcheck disable=SC2181 37 | [ "$?" -eq 0 ] || error "could not find sandbox directory $SANDBOX_DIR (could not cd in)" 2 38 | else 39 | ## Create a new sandbox if one was not given 40 | SANDBOX_DIR="$(mktemp -d --suffix ".try-$EXECID")" 41 | fi 42 | 43 | ## If the sandbox is not valid we exit early 44 | if ! sandbox_valid_or_empty "$SANDBOX_DIR" 45 | then 46 | error "given sandbox '$SANDBOX_DIR' is invalid" 1 47 | fi 48 | 49 | ## Make any directories that don't already exist, this is OK to do here 50 | ## because we have already checked if it valid. 51 | export SANDBOX_DIR 52 | 53 | # We created "$IGNORE_FILE" up front, but now we can stash it in the sandbox. 54 | mv "$IGNORE_FILE" "$SANDBOX_DIR"/ignore 55 | IGNORE_FILE="$SANDBOX_DIR"/ignore 56 | 57 | try_mount_log="$SANDBOX_DIR"/mount.log 58 | export try_mount_log 59 | 60 | # If we're in a docker container, we want to mount tmpfs on sandbox_dir, #136 61 | # tail -n +2 to ignore the first line with the column name 62 | tmpfstype=$(df --output=fstype "$SANDBOX_DIR" | tail -n +2) 63 | if [ "$tmpfstype" = "overlay" ] && [ "$(id -u)" -eq "0" ] 64 | then 65 | echo "mounting sandbox '$SANDBOX_DIR' as tmpfs (underlying fs is overlayfs)" >> "$try_mount_log" 66 | echo "consider docker volumes if you want persistence" >> "$try_mount_log" 67 | mount -t tmpfs tmpfs "$SANDBOX_DIR" 68 | fi 69 | 70 | mkdir -p "$SANDBOX_DIR/upperdir" "$SANDBOX_DIR/workdir" "$SANDBOX_DIR/temproot" 71 | 72 | ## Find all the directories and mounts that need to be mounted 73 | DIRS_AND_MOUNTS="$SANDBOX_DIR"/mounts 74 | export DIRS_AND_MOUNTS 75 | find / -maxdepth 1 >"$DIRS_AND_MOUNTS" 76 | sort -u -o "$DIRS_AND_MOUNTS" "$DIRS_AND_MOUNTS" 77 | 78 | # Calculate UPDATED_DIRS_AND_MOUNTS that contains the merge arguments in LOWER_DIRS 79 | UPDATED_DIRS_AND_MOUNTS="$SANDBOX_DIR"/mounts.updated 80 | export UPDATED_DIRS_AND_MOUNTS 81 | while IFS="" read -r mountpoint 82 | do 83 | new_mountpoint="" 84 | OLDIFS=$IFS 85 | IFS=":" 86 | 87 | for lower_dir in $LOWER_DIRS 88 | do 89 | temp_mountpoint="$lower_dir/upperdir$mountpoint" 90 | # Make sure we put : between, but not at the beginning 91 | new_mountpoint="${new_mountpoint:+$new_mountpoint:}$temp_mountpoint" 92 | done 93 | IFS=$OLDIFS 94 | # Add the original mountpoint at the end 95 | new_mountpoint="${new_mountpoint:+$new_mountpoint:}$mountpoint" 96 | echo "$new_mountpoint" >> "$UPDATED_DIRS_AND_MOUNTS" 97 | done <"$DIRS_AND_MOUNTS" 98 | 99 | 100 | # we will overlay-mount each root directory separately (instead of all at once) because some directories cannot be overlayed 101 | # so we set up the mount points now 102 | # 103 | # KK 2023-06-29 This approach (of mounting each root directory separately) was necessary because we could not mount `/` in an overlay. 104 | # However, this might be solvable using mergerfs/unionfs, allowing us to mount an overlay on a unionfs of the `/` once. 105 | while IFS="" read -r mountpoint 106 | do 107 | ## Only make the directory if the original is a directory too 108 | if [ -d "$mountpoint" ] && ! [ -L "$mountpoint" ] 109 | then 110 | # shellcheck disable=SC2174 # warning acknowledged, "When used with -p, -m only applies to the deepest directory." 111 | mkdir -m "$(stat -c %a "$mountpoint")" -p "${SANDBOX_DIR}/upperdir/${mountpoint}" "${SANDBOX_DIR}/workdir/${mountpoint}" "${SANDBOX_DIR}/temproot/${mountpoint}" 112 | fi 113 | done <"$DIRS_AND_MOUNTS" 114 | 115 | chmod "$(stat -c %a /)" "$SANDBOX_DIR/temproot" 116 | 117 | mount_and_execute="$SANDBOX_DIR"/mount_and_execute.sh 118 | chroot_executable="$SANDBOX_DIR"/chroot_executable.sh 119 | script_to_execute="$SANDBOX_DIR"/script_to_execute.sh 120 | 121 | export chroot_executable 122 | export script_to_execute 123 | 124 | cat >"$mount_and_execute" <<"EOF" 125 | #!/bin/sh 126 | 127 | TRY_COMMAND="$TRY_COMMAND($0)" 128 | 129 | ## A wrapper of `mount -t overlay` to have cleaner looking code 130 | make_overlay() { 131 | sandbox_dir="$1" 132 | lowerdirs="$2" 133 | overlay_mountpoint="$3" 134 | mount -t overlay overlay -o userxattr -o "lowerdir=$lowerdirs,upperdir=$sandbox_dir/upperdir/$overlay_mountpoint,workdir=$sandbox_dir/workdir/$overlay_mountpoint" "$sandbox_dir/temproot/$overlay_mountpoint" 135 | } 136 | 137 | 138 | devices_to_mount="tty null zero full random urandom" 139 | 140 | ## Mounts and unmounts a few select devices instead of the whole `/dev` 141 | mount_devices() { 142 | sandbox_dir="$1" 143 | for dev in $devices_to_mount 144 | do 145 | touch "$sandbox_dir/temproot/dev/$dev" 146 | mount -o bind /dev/$dev "$sandbox_dir/temproot/dev/$dev" 147 | done 148 | } 149 | 150 | unmount_devices() { 151 | sandbox_dir="$1" 152 | for dev in $devices_to_mount 153 | do 154 | umount "$sandbox_dir/temproot/dev/$dev" 2>>"$try_mount_log" 155 | rm -f "$sandbox_dir/temproot/dev/$dev" 156 | done 157 | } 158 | 159 | ## Try to autodetect union helper: {mergerfs | unionfs} 160 | ## Returns an empty string if no union helper is found 161 | autodetect_union_helper() { 162 | if command -v mergerfs >/dev/null; then 163 | UNION_HELPER=mergerfs 164 | elif command -v unionfs >/dev/null; then 165 | UNION_HELPER=unionfs 166 | fi 167 | } 168 | 169 | # Detect if union_helper is set, if not, we try to autodetect them 170 | if [ -z "$UNION_HELPER" ] 171 | then 172 | ## Try to detect the union_helper (the variable could still be empty afterwards). 173 | autodetect_union_helper 174 | fi 175 | 176 | # actually mount the overlays 177 | for mountpoint in $(cat "$UPDATED_DIRS_AND_MOUNTS") 178 | do 179 | pure_mountpoint=${mountpoint##*:} 180 | 181 | ## We are not interested in mounts that are not directories 182 | if ! [ -d "$pure_mountpoint" ] 183 | then 184 | continue 185 | fi 186 | 187 | ## Symlinks 188 | if [ -L "$pure_mountpoint" ] 189 | then 190 | ln -s $(readlink "$pure_mountpoint") "$SANDBOX_DIR/temproot/$pure_mountpoint" 191 | continue 192 | fi 193 | 194 | ## Don't do anything for the root and skip if it is /dev or /proc, we will mount it later 195 | case "$pure_mountpoint" in 196 | (/|/dev|/proc) continue;; 197 | esac 198 | 199 | # Try mounting everything normally 200 | make_overlay "$SANDBOX_DIR" "$mountpoint" "$pure_mountpoint" 2>>"$try_mount_log" 201 | # If mounting everything normally fails, we try using either using mergerfs or unionfs to mount them. 202 | if [ "$?" -ne 0 ] 203 | then 204 | ## If the overlay failed, it means that there is a nested mount inside the target mount, e.g., both `/home` and `/home/user/mnt` are mounts. 205 | ## To address this, we use unionfs/mergerfs (they support the same functionality) to show all mounts under the target mount as normal directories. 206 | ## Then we can normally make the overlay on the new union directory. 207 | ## 208 | ## MMG 2025-01-27 209 | ## There used to be more complicated logic here using `findmnt`, but we currently 210 | ## just build unions for every mount in the root. 211 | 212 | if [ -z "$UNION_HELPER" ] 213 | then 214 | ## We can ignore this mountpoint, if the user program tries to use it, it will crash, but if not we can run normally 215 | printf "%s: Warning: Failed mounting $mountpoint as an overlay and mergerfs or unionfs not set and could not be found, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2 216 | else 217 | merger_dir="$SANDBOX_DIR"/mergerdir"$(echo "$pure_mountpoint" | tr '/' '.')" 218 | mkdir "$merger_dir" 219 | 220 | ## Create a union directory 221 | "$UNION_HELPER" $mountpoint $merger_dir 2>>"$try_mount_log" || 222 | printf "%s: Warning: Failed mounting $mountpoint via $UNION_HELPER, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2 223 | make_overlay "$SANDBOX_DIR" "$merger_dir" "$pure_mountpoint" 2>>"$try_mount_log" || 224 | printf "%s: Warning: Failed mounting $mountpoint as an overlay via $UNION_HELPER, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2 225 | fi 226 | fi 227 | done 228 | 229 | ## Mount a few select devices in /dev 230 | mount_devices "$SANDBOX_DIR" 231 | 232 | ## Check if chroot_executable exists, #29 233 | if ! [ -f "$SANDBOX_DIR/temproot/$chroot_executable" ] 234 | then 235 | cp $chroot_executable "$SANDBOX_DIR/temproot/$chroot_executable" 236 | fi 237 | 238 | unshare --root="$SANDBOX_DIR/temproot" /bin/sh "$chroot_executable" 239 | exitcode="$?" 240 | 241 | # unmount the devices 242 | rm "$sandbox_dir/temproot/dev/stdin" 243 | rm "$sandbox_dir/temproot/dev/stdout" 244 | rm "$sandbox_dir/temproot/dev/stderr" 245 | 246 | unmount_devices "$SANDBOX_DIR" 247 | 248 | exit $exitcode 249 | EOF 250 | 251 | # NB we substitute in the heredoc, so the early unsets are okay! 252 | cat >"$chroot_executable" <"$script_to_execute" 267 | 268 | # `$script_to_execute` need not be +x to be sourced 269 | chmod +x "$mount_and_execute" "$chroot_executable" 270 | 271 | # enable job control so interactive commands will play nicely with try asking for user input later(for committing). #5 272 | [ -t 0 ] && set -m 273 | 274 | # --mount: mounting and unmounting filesystems will not affect the rest of the system outside the unshare 275 | # --map-root-user: map to the superuser UID and GID in the newly created user namespace. 276 | # --user: the process will have a distinct set of UIDs, GIDs and capabilities. 277 | # --pid: create a new process namespace (needed fr procfs to work right) 278 | # --fork: necessary if we do --pid 279 | # "Creation of a persistent PID namespace will fail if the --fork option is not also specified." 280 | # shellcheck disable=SC2086 # we want field splitting! 281 | unshare --mount --map-root-user --user --pid --fork $EXTRA_NS "$mount_and_execute" 282 | TRY_EXIT_STATUS=$? 283 | 284 | # remove symlink 285 | # first set temproot to be writible, rhel derivatives defaults / to r-xr-xr-x 286 | chmod 755 "${SANDBOX_DIR}/temproot" 287 | while IFS="" read -r mountpoint 288 | do 289 | pure_mountpoint=${mountpoint##*:} 290 | if [ -L "$pure_mountpoint" ] 291 | then 292 | rm "${SANDBOX_DIR}/temproot/${mountpoint}" 293 | fi 294 | done <"$DIRS_AND_MOUNTS" 295 | 296 | ################################################################################ 297 | # commit? 298 | 299 | case "$NO_COMMIT" in 300 | (quiet) ;; 301 | (show) echo "$SANDBOX_DIR";; 302 | (commit) commit;; 303 | (interactive) summary >&2 304 | # shellcheck disable=SC2181 305 | if [ "$?" -eq 0 ] 306 | then 307 | printf "\nCommit these changes? [y/N] " >&2 308 | read -r DO_COMMIT 309 | case "$DO_COMMIT" in 310 | (y|Y|yes|YES) commit;; 311 | (*) echo "Not committing." >&2 312 | echo "$SANDBOX_DIR";; 313 | esac 314 | fi;; 315 | esac 316 | } 317 | 318 | ################################################################################ 319 | # Summarize the overlay in `$SANDBOX_DIR` 320 | ################################################################################ 321 | 322 | if type try-summary >/dev/null 2>&1 323 | then 324 | summary() { 325 | try-summary -i "$IGNORE_FILE" "$SANDBOX_DIR" || return 1 326 | TRY_EXIT_STATUS=0 327 | } 328 | else 329 | summary() { 330 | if ! [ -d "$SANDBOX_DIR" ] 331 | then 332 | error "could not find directory $SANDBOX_DIR" 2 333 | elif ! [ -d "$SANDBOX_DIR/upperdir" ] 334 | then 335 | error "could not find directory $SANDBOX_DIR/upperdir" 1 336 | fi 337 | 338 | ## Finds all potential changes 339 | changed_files=$(find_upperdir_changes "$SANDBOX_DIR" "$IGNORE_FILE") 340 | summary_output=$(process_changes "$SANDBOX_DIR" "$changed_files") 341 | if [ -z "$summary_output" ] 342 | then 343 | return 1 344 | fi 345 | 346 | echo 347 | echo "Changes detected in the following files:" 348 | echo 349 | 350 | echo "$summary_output" | while IFS= read -r summary_line 351 | do 352 | local_file="$(echo "$summary_line" | cut -c 4-)" 353 | case "$summary_line" in 354 | (ln*) echo "$local_file (symlink)";; 355 | (rd*) echo "$local_file (replaced with dir)";; 356 | (md*) echo "$local_file (created dir)";; 357 | (de*) echo "$local_file (deleted)";; 358 | (mo*) echo "$local_file (modified)";; 359 | (ad*) echo "$local_file (added)";; 360 | esac 361 | done 362 | 363 | TRY_EXIT_STATUS=0 364 | } 365 | fi 366 | 367 | ################################################################################ 368 | # Commit the results of an overlay in `$SANDBOX_DIR` 369 | ################################################################################ 370 | 371 | if type try-commit >/dev/null 2>&1 372 | then 373 | commit() { 374 | try-commit -i "$IGNORE_FILE" "$SANDBOX_DIR" 375 | TRY_EXIT_STATUS=$? 376 | } 377 | else 378 | commit() { 379 | if ! [ -d "$SANDBOX_DIR" ] 380 | then 381 | error "could not find directory $SANDBOX_DIR" "$TRY_COMMAND" 2 382 | elif ! [ -d "$SANDBOX_DIR/upperdir" ] 383 | then 384 | error "could not find directory $SANDBOX_DIR/upperdir" 1 385 | fi 386 | 387 | changed_files=$(find_upperdir_changes "$SANDBOX_DIR" "$IGNORE_FILE") 388 | summary_output=$(process_changes "$SANDBOX_DIR" "$changed_files") 389 | 390 | TRY_EXIT_STATUS=0 391 | echo "$summary_output" | while IFS= read -r summary_line; do 392 | local_file="$(echo "$summary_line" | cut -c 4-)" 393 | changed_file="$SANDBOX_DIR/upperdir$local_file" 394 | case $summary_line in 395 | (ln*) rm -rf "$local_file"; ln -s "$(readlink "$changed_file")" "$local_file";; 396 | (rd*) rm -rf "$local_file"; mkdir "$local_file";; 397 | (md*) mkdir "$local_file";; 398 | (de*) rm -rf "$local_file";; 399 | (mo*) rm -rf "$local_file"; mv "$changed_file" "$local_file";; 400 | (ad*) mv "$changed_file" "$local_file";; 401 | esac 402 | 403 | # shellcheck disable=SC2181 404 | if [ "$?" -ne 0 ] 405 | then 406 | warn "couldn't commit $changed_file" 407 | TRY_EXIT_STATUS=1 408 | fi 409 | done 410 | } 411 | fi 412 | 413 | ################################################################################ 414 | ## Defines which changes we want to ignore in the summary and commit 415 | ################################################################################ 416 | 417 | ignore_changes() { 418 | ignore_file="$1" 419 | 420 | grep -v -f "$ignore_file" 421 | } 422 | 423 | ################################################################################ 424 | ## Lists all upperdir changes in raw format 425 | ################################################################################ 426 | 427 | find_upperdir_changes() { 428 | sandbox_dir="$1" 429 | ignore_file="$2" 430 | 431 | find "$sandbox_dir/upperdir/" -type f -o \( -type c -size 0 \) -o -type d -o -type l | ignore_changes "$ignore_file" 432 | } 433 | 434 | ################################################################################ 435 | # Processes upperdir changes to an internal format that can be processed by summary and commit 436 | # 437 | # Output format: 438 | # 439 | # XX PATH 440 | # 441 | # where: 442 | # XX is a two character code for the modification 443 | # - rd: Replaced with a directory 444 | # - md: Created a directory 445 | # - de: Deleted a file 446 | # - mo: Modified a file 447 | # - ad: Added a file 448 | # 449 | # PATH is the local/host path (i.e., without the upper 450 | ################################################################################ 451 | 452 | process_changes() { 453 | sandbox_dir="$1" 454 | changed_files="$2" 455 | 456 | while IFS= read -r changed_file 457 | do 458 | local_file="${changed_file#"$sandbox_dir/upperdir"}" 459 | if [ -L "$changed_file" ] 460 | then 461 | # // TRYCASE(symlink, *) 462 | echo "ln $local_file" 463 | elif [ -d "$changed_file" ] 464 | then 465 | if ! [ -e "$local_file" ] 466 | then 467 | # // TRYCASE(dir, nonexist) 468 | echo "md $local_file" 469 | continue 470 | fi 471 | 472 | if [ "$(getfattr --absolute-names --only-values --e text -n user.overlay.opaque "$changed_file" 2>/dev/null)" = "y" ] 473 | then 474 | # // TRYCASE(opaque, *) 475 | # // TRYCASE(dir, dir) 476 | echo "rd $local_file" 477 | continue 478 | fi 479 | 480 | if ! [ -d "$local_file" ] 481 | then 482 | # // TRYCASE(dir, file) 483 | # // TRYCASE(dir, symlink) 484 | echo "rd $local_file" 485 | continue 486 | fi 487 | 488 | # must be a directory, but not opaque---leave it! 489 | elif [ -c "$changed_file" ] && ! [ -s "$changed_file" ] && [ "$(stat -c %t,%T "$changed_file")" = "0,0" ] 490 | then 491 | # // TRYCASE(whiteout, *) 492 | echo "de $local_file" 493 | elif [ -f "$changed_file" ] 494 | then 495 | if [ -f "$changed_file" ] && getfattr --absolute-names -d "$changed_file" 2>/dev/null | grep -q -e "user.overlay.whiteout" 496 | then 497 | # // TRYCASE(whiteout, *) 498 | echo "de $local_file" 499 | continue 500 | fi 501 | 502 | if [ -e "$local_file" ] 503 | then 504 | # // TRYCASE(file, file) 505 | # // TRYCASE(file, dir) 506 | # // TRYCASE(file, symlink) 507 | echo "mo $local_file" 508 | else 509 | # // TRYCASE(file, nonexist) 510 | echo "ad $local_file" 511 | fi 512 | fi 513 | done <&2 553 | } 554 | 555 | ################################################################################ 556 | # Emit a warning and exit 557 | ################################################################################ 558 | 559 | error() { 560 | msg="$1" 561 | exit_status="$2" 562 | 563 | warn "$msg" 564 | exit "$exit_status" 565 | } 566 | 567 | ################################################################################ 568 | # Argument parsing 569 | ################################################################################ 570 | 571 | usage() { 572 | cat >&2 <>"$IGNORE_FILE";; 614 | (D) if ! [ -d "$OPTARG" ] 615 | then 616 | error "could not find sandbox directory '$OPTARG'" 2 617 | fi 618 | SANDBOX_DIR="$OPTARG" 619 | NO_COMMIT="quiet";; 620 | (L) if [ -n "$LOWER_DIRS" ] 621 | then 622 | error "the -L option has been specified multiple times" 2 623 | fi 624 | LOWER_DIRS="$OPTARG" 625 | NO_COMMIT="quiet";; 626 | (v) echo "$TRY_COMMAND version $TRY_VERSION" >&2 627 | exit 0;; 628 | (U) if ! [ -x "$OPTARG" ] 629 | then 630 | error "could not find executable union helper '$OPTARG'" 2 631 | fi 632 | UNION_HELPER="$OPTARG" 633 | export UNION_HELPER;; 634 | (x) EXTRA_NS="--net";; 635 | (h|*) usage 636 | exit 0;; 637 | esac 638 | done 639 | 640 | shift $((OPTIND - 1)) 641 | 642 | if [ "$#" -eq 0 ] 643 | then 644 | usage 645 | exit 2 646 | fi 647 | 648 | TRY_EXIT_STATUS=1 649 | case "$1" in 650 | (summary) : "${SANDBOX_DIR=$2}" 651 | summary 652 | rm "$IGNORE_FILE" # we didn't move it to the sandbox, so clean up 653 | ;; 654 | (commit) : "${SANDBOX_DIR=$2}" 655 | commit 656 | rm "$IGNORE_FILE" # we didn't move it to the sandbox, so clean up 657 | ;; 658 | (explore) : "${SANDBOX_DIR=$2}" 659 | try "$SHELL";; 660 | (--) shift 661 | try "$@";; 662 | (*) try "$@";; 663 | esac 664 | 665 | exit "$TRY_EXIT_STATUS" 666 | -------------------------------------------------------------------------------- /utils/.gitignore: -------------------------------------------------------------------------------- 1 | try-summary 2 | try-commit 3 | *.o 4 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | This directory contains C code (GNU dialect, Linux only) that summarizes and commits the changes from `try`, in `try-summary` and `try-commit`, respectively. 2 | 3 | You do _not_ need these utilities to use `try`, which has POSIX shell fallback implementations. 4 | 5 | Every time `try` starts up, it looks for these executables on its path; if found, it will use them---otherwise it will use the shell fallbacks. The shell fallbacks are not quite as robust as these utilities and may behave strangely on files with newlines in their names. 6 | 7 | # Development 8 | 9 | These are meant to short, simple scripts. 10 | 11 | In order to make sure that we keep them parallel, both files are annotated with `TRYCASE(changed, local)` forms in comments that identify which case is being handled. 12 | 13 | Here `changed` and `local` adhere to `lf` the following grammar: 14 | 15 | ``` 16 | changed ::= f | opaque | whiteout 17 | local ::= f | * 18 | f ::= file | dir | symlink | nonexist 19 | ``` 20 | 21 | The script `../scripts/check_trycase.sh` will check to make sure that all cases are considered (excepting `TRYCASE(opaque, nonexist)`, which should not happen). If new files need to do a case analysis on how changed files and local files relate, annotate each case with `// TRYCASE(cf, lf)` and add a `check_file` call to the `check_trycase.sh` script. 22 | 23 | ## Assumptions 24 | 25 | We rely on `user.overlay.opaque` to be set for opaque directories, which we can assume will happen because `try` sets `-o userxattr` when it mounts the overlay. 26 | 27 | We do not handle [redirect directories](https://docs.kernel.org/filesystems/overlayfs.html#redirect-dir), instead relying on `cp`/`mv` to do the copying. 28 | 29 | We have not seen `user.overlay.whiteout` in practice---perhaps it only gets made when you can't `mknod` a `(0,0)` character device on the upperdir filesystem? 30 | -------------------------------------------------------------------------------- /utils/ignores.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | 5 | #include "ignores.h" 6 | 7 | static regex_t *ignores = NULL; 8 | static size_t ignores_len = 0; 9 | static int num_ignores = 0; 10 | 11 | int should_ignore(char *filename) { 12 | for (int i = 0; i < num_ignores; i += 1) { 13 | if (regexec(&ignores[i], filename, 0, NULL, 0) == 0) { 14 | return 1; 15 | } 16 | } 17 | 18 | return 0; 19 | } 20 | 21 | void load_ignores(char *progname, char *ignore_filename) { 22 | FILE *ignore_file = fopen(ignore_filename, "r"); 23 | if (ignore_file == NULL) { 24 | fprintf(stderr, "%s: couldn't load ignore file '%s'\n", progname, ignore_filename); 25 | return; 26 | } 27 | 28 | ignores_len = 4; 29 | ignores = (regex_t *) malloc(sizeof(regex_t) * ignores_len); 30 | num_ignores = 0; 31 | 32 | char *line = NULL; 33 | size_t len = 0; 34 | ssize_t nread; 35 | 36 | while ((nread = getline(&line, &len, ignore_file)) != -1) { 37 | if (num_ignores == ignores_len) { 38 | ignores_len *= 2; 39 | ignores = realloc(ignores, sizeof(regex_t) * ignores_len); 40 | } 41 | 42 | line[nread-1] = '\0'; // kill newline before compilation 43 | int errcode = 0; 44 | if ((errcode = regcomp(&ignores[num_ignores], line, REG_EXTENDED | REG_NOSUB)) != 0) { 45 | size_t err_len = regerror(errcode, &ignores[num_ignores], NULL, 0); 46 | char err[err_len]; 47 | regerror(errcode, &ignores[num_ignores], err, err_len); 48 | 49 | fprintf(stderr, "%s: couldn't process regex '%s': %s\n", progname, line, err); 50 | continue; 51 | } 52 | 53 | num_ignores += 1; 54 | } 55 | 56 | free(line); 57 | fclose(ignore_file); 58 | } 59 | 60 | void free_ignores() { 61 | for (int i = 0; i < num_ignores; i += 1) { 62 | regfree(&ignores[i]); 63 | } 64 | free(ignores); 65 | ignores = NULL; 66 | num_ignores = 0; 67 | ignores_len = 0; 68 | } 69 | -------------------------------------------------------------------------------- /utils/ignores.h: -------------------------------------------------------------------------------- 1 | #ifndef __TRY_IGNORES_H 2 | #define __TRY_IGNORES_H 3 | 4 | #include 5 | #include 6 | 7 | void load_ignores(char *progname, char *ignore_file); 8 | int should_ignore(char *filename); 9 | void free_ignores(); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /utils/try-commit.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "ignores.h" 18 | #include "version.h" 19 | 20 | static int should_copy = 0; 21 | 22 | static int num_errors = 0; 23 | void commit_error(char *changed_file, char *msg) { 24 | num_errors += 1; 25 | 26 | fprintf(stderr, "try-commit: couldn't commit %s (%s): %s\n", changed_file, msg, strerror(errno)); 27 | } 28 | 29 | void run(char *argv[], char *file) { 30 | pid_t pid = fork(); 31 | 32 | if (pid == 0) { 33 | execvp(argv[0], argv); 34 | return; // unreachable 35 | } 36 | 37 | int status = -1; 38 | waitpid(pid, &status, 0); 39 | 40 | if (status != 0) { 41 | commit_error(file, argv[0]); 42 | } 43 | } 44 | 45 | void commit(char *changed_file, char *local_file) { 46 | if (should_copy) { 47 | char *argv[5] = { "cp", "-fa", changed_file, local_file, NULL }; 48 | run(argv, changed_file); 49 | return; 50 | } 51 | 52 | if (rename(changed_file, local_file) != 0) { 53 | if (errno != EXDEV) { 54 | commit_error(changed_file, "rename"); 55 | return; 56 | } 57 | 58 | // cross-device move... not a simple call. defer to system utility 59 | char *argv[4] = { "mv", changed_file, local_file, NULL }; 60 | run(argv, changed_file); 61 | } 62 | } 63 | 64 | void remove_local(char *local_file, int local_exists, struct stat *local_stat) { 65 | if (!local_exists) { 66 | return; 67 | } 68 | 69 | if (S_ISDIR(local_stat->st_mode)) { 70 | char *argv[4] = { "rm", "-rf", local_file, NULL }; 71 | run(argv, local_file); 72 | return; 73 | } 74 | 75 | if (unlink(local_file) != 0) { 76 | commit_error(local_file, "rm"); 77 | } 78 | } 79 | 80 | void usage(int status) { 81 | fprintf(stderr, "Usage: try-commit [-c] [-i IGNORE_FILE] SANDBOX_DIR\n"); 82 | fprintf(stderr, "\t-c\tcopy files instead of moving them\n"); 83 | exit(status); 84 | } 85 | 86 | int main(int argc, char *argv[]) { 87 | int opt; 88 | while ((opt = getopt(argc, argv, "hvci:")) != -1) { 89 | switch (opt) { 90 | case 'i': 91 | load_ignores("try-commit", optarg); 92 | break; 93 | case 'c': 94 | should_copy = 1; 95 | break; 96 | case 'v': 97 | fprintf(stderr, "try-commit version " TRY_VERSION "\n"); 98 | exit(0); 99 | case 'h': 100 | usage(0); 101 | default: 102 | usage(2); 103 | } 104 | } 105 | 106 | if (argc != optind + 1) { 107 | usage(2); 108 | } 109 | 110 | struct stat sandbox_stat; 111 | if (stat(argv[optind], &sandbox_stat) == -1) { 112 | perror("try-commit: could not find sandbox dircteory"); 113 | return 2; 114 | } 115 | 116 | if (!S_ISDIR(sandbox_stat.st_mode)) { 117 | perror("try-commit: sandbox is not a directory"); 118 | return 2; 119 | } 120 | 121 | size_t prefix_len = strlen(argv[optind]); 122 | 123 | // trim final slashes 124 | while (argv[optind][prefix_len-1] == '/') { 125 | argv[optind][prefix_len-1] = '\0'; 126 | prefix_len -= 1; 127 | } 128 | 129 | char upperdir_path[prefix_len + 10]; 130 | strncpy(upperdir_path, argv[optind], prefix_len); 131 | strncpy(upperdir_path + prefix_len, "/upperdir", 10); 132 | prefix_len += 9; 133 | 134 | char *dirs[2] = { upperdir_path, NULL }; 135 | 136 | FTS *fts = fts_open(dirs, FTS_PHYSICAL, NULL); 137 | 138 | if (fts == NULL) { 139 | perror("try-commit: fts_open"); 140 | return 2; 141 | } 142 | 143 | FTSENT *ent = fts_read(fts); 144 | 145 | // skip first entry (the named directory, corresponds to root) 146 | if (ent == NULL) { 147 | perror("try-commit: fts_read"); 148 | return 2; 149 | } 150 | assert(strcmp(ent->fts_path, upperdir_path) == 0); 151 | if (ent->fts_info != FTS_D) { 152 | fprintf(stderr, "try-commit: sandbox upperdir '%s' is not a directory\n", ent->fts_path); 153 | return 1; 154 | } 155 | 156 | while ((ent = fts_read(fts)) != NULL) { 157 | char *local_file = ent->fts_path + prefix_len; 158 | 159 | if (should_ignore(local_file)) { continue; } 160 | 161 | struct stat local_stat; 162 | int local_exists = lstat(local_file, &local_stat) != -1; 163 | 164 | switch (ent->fts_info) { 165 | case FTS_D: // preorder (first visit) 166 | if (!local_exists) { 167 | // TRYCASE(dir, nonexist) 168 | commit(ent->fts_path, local_file); 169 | 170 | // don't traverse children, we copied the whole thing 171 | fts_set(fts, ent, FTS_SKIP); 172 | break; 173 | } 174 | 175 | // special "OPAQUE" whiteout directory--delete the original 176 | char xattr_buf[2] = { '\0', '\0' }; 177 | if (getxattr(ent->fts_path, "user.overlay.opaque", xattr_buf, 2) != -1 && xattr_buf[0] == 'y') { 178 | // TRYCASE(opaque, *) 179 | // TRYCASE(dir, dir) 180 | remove_local(local_file, local_exists, &local_stat); 181 | 182 | commit(ent->fts_path, local_file); 183 | 184 | // don't traverse children, we copied the whole thing 185 | fts_set(fts, ent, FTS_SKIP); 186 | break; 187 | } 188 | 189 | // non-directory replaced with a directory 190 | if (!S_ISDIR(local_stat.st_mode)) { 191 | // TRYCASE(dir, file) 192 | // TRYCASE(dir, symlink) 193 | 194 | if (unlink(local_file) != 0) { 195 | commit_error(ent->fts_path, "rm"); 196 | } 197 | 198 | commit(ent->fts_path, local_file); 199 | 200 | // don't traverse children, we copied the whole thing 201 | fts_set(fts, ent, FTS_SKIP); 202 | break; 203 | } 204 | 205 | // nothing of interest! directory got made, but modifications must be inside 206 | break; 207 | case FTS_F: // regular file 208 | if (getxattr(ent->fts_path, "user.overlay.whiteout", NULL, 0) != -1) { 209 | // TRYCASE(whiteout, *) 210 | remove_local(local_file, local_exists, &local_stat); 211 | break; 212 | } 213 | 214 | if (local_exists) { 215 | // TRYCASE(file, file) 216 | // TRYCASE(file, dir) 217 | // TRYCASE(file, symlink) 218 | remove_local(local_file, local_exists, &local_stat); 219 | } 220 | 221 | // TRYCASE(file, nonexist) 222 | commit(ent->fts_path, local_file); 223 | break; 224 | 225 | case FTS_SL: // symbolic link 226 | case FTS_SLNONE: // dangling symbolic link 227 | // TRYCASE(symlink, *) 228 | remove_local(local_file, local_exists, &local_stat); 229 | 230 | // absolute shenanigans: what's the target (and how long is its name)? 231 | size_t tgt_len = ent->fts_statp->st_size + 1; 232 | if (tgt_len <= 1) { // procfs (and possibly others) return `st_size` of 0 :( 233 | tgt_len = PATH_MAX; 234 | } 235 | 236 | char *tgt = malloc(sizeof(char) * tgt_len); 237 | int nbytes = readlink(ent->fts_path, tgt, tgt_len); 238 | if (nbytes == -1) { 239 | commit_error(ent->fts_path, "ln -s"); 240 | } 241 | 242 | while (nbytes == tgt_len) { 243 | tgt_len *= 2; 244 | tgt = realloc(tgt, sizeof(char) * tgt_len); 245 | nbytes = readlink(ent->fts_path, tgt, tgt_len); 246 | if (nbytes == -1) { 247 | commit_error(ent->fts_path, "ln -s"); 248 | } 249 | } 250 | tgt[nbytes] = '\0'; // readlink doesn't put a null byte on the end lol nice work everyone 251 | 252 | if (symlink(tgt, local_file) != 0) { 253 | commit_error(ent->fts_path, "ln -s"); 254 | } 255 | free(tgt); 256 | break; 257 | 258 | case FTS_DEFAULT: 259 | if (S_ISCHR(ent->fts_statp->st_mode) && ent->fts_statp->st_size == 0) { 260 | struct statx statxp; 261 | if (statx(AT_FDCWD, ent->fts_path, 0, STATX_TYPE | STATX_INO, &statxp) == -1) { 262 | fprintf(stderr, "try-commit: statx: %s: %s\n", ent->fts_path, strerror(errno)); 263 | break; 264 | } 265 | 266 | if (statxp.stx_rdev_major == 0 && statxp.stx_rdev_minor == 0) { 267 | // TRYCASE(whiteout, *) 268 | remove_local(local_file, local_exists, &local_stat); 269 | 270 | break; 271 | } 272 | } 273 | 274 | case FTS_DC: // cycle 275 | case FTS_DP: // postorder (second visit) 276 | break; 277 | 278 | case FTS_DNR: 279 | case FTS_ERR: 280 | default: 281 | fprintf(stderr, "try-commit: fts_read: %s: %s\n", ent->fts_path, strerror(errno)); 282 | break; // error with this directory 283 | } 284 | } 285 | 286 | if (errno != 0) { 287 | perror("try-commit: fts_read"); 288 | return 2; 289 | } 290 | 291 | fts_close(fts); 292 | free_ignores(); 293 | 294 | return num_errors == 0 ? 0 : 1; 295 | } 296 | -------------------------------------------------------------------------------- /utils/try-summary.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "ignores.h" 15 | #include "version.h" 16 | 17 | static int changes_detected = 0; 18 | void show_change(char *local_file, char *msg) { 19 | if (!changes_detected) { 20 | changes_detected += 1; 21 | fputs("\nChanges detected in the following files:\n\n", stdout); 22 | } 23 | 24 | fputs(local_file, stdout); 25 | fputs(" (", stdout); 26 | fputs(msg, stdout); 27 | fputs(")\n", stdout); 28 | } 29 | 30 | void usage(int status) { 31 | fprintf(stderr, "Usage: try-summary [-i IGNORE_FILE] SANDBOX_DIR\n"); 32 | exit(status); 33 | } 34 | 35 | int main(int argc, char *argv[]) { 36 | int opt; 37 | while ((opt = getopt(argc, argv, "hvi:")) != -1) { 38 | switch (opt) { 39 | case 'i': 40 | load_ignores("try-summary", optarg); 41 | break; 42 | case 'v': 43 | fprintf(stderr, "try-summary version " TRY_VERSION "\n"); 44 | exit(0); 45 | case 'h': 46 | usage(0); 47 | default: 48 | usage(2); 49 | } 50 | } 51 | 52 | if (argc != optind + 1) { 53 | usage(2); 54 | } 55 | 56 | struct stat sandbox_stat; 57 | if (stat(argv[optind], &sandbox_stat) == -1) { 58 | perror("try-summary: could not find sandbox dircteory"); 59 | return 2; 60 | } 61 | 62 | if (!S_ISDIR(sandbox_stat.st_mode)) { 63 | perror("try-summary: sandbox is not a directory"); 64 | return 2; 65 | } 66 | 67 | size_t prefix_len = strlen(argv[optind]); 68 | 69 | // trim final slashes 70 | while (argv[optind][prefix_len-1] == '/') { 71 | argv[optind][prefix_len-1] = '\0'; 72 | prefix_len -= 1; 73 | } 74 | 75 | char upperdir_path[prefix_len + 10]; 76 | strncpy(upperdir_path, argv[optind], prefix_len); 77 | strncpy(upperdir_path + prefix_len, "/upperdir", 10); 78 | prefix_len += 9; 79 | 80 | char *dirs[2] = { upperdir_path, NULL }; 81 | 82 | FTS *fts = fts_open(dirs, FTS_PHYSICAL, NULL); 83 | 84 | if (fts == NULL) { 85 | perror("try-summary: fts_open"); 86 | return 2; 87 | } 88 | 89 | FTSENT *ent = fts_read(fts); 90 | 91 | // skip first entry (the named directory, corresponds to root) 92 | if (ent == NULL) { 93 | perror("try-summary: fts_read"); 94 | return 2; 95 | } 96 | assert(strcmp(ent->fts_path, upperdir_path) == 0); 97 | if (ent->fts_info != FTS_D) { 98 | fprintf(stderr, "try-summary: %s is not a directory\n", ent->fts_path); 99 | return 1; 100 | } 101 | 102 | while ((ent = fts_read(fts)) != NULL) { 103 | char *local_file = ent->fts_path + prefix_len; 104 | 105 | if (should_ignore(local_file)) { continue; } 106 | 107 | struct stat local_stat; 108 | int local_exists = lstat(local_file, &local_stat) != -1; 109 | 110 | switch (ent->fts_info) { 111 | case FTS_D: // preorder (first visit) 112 | if (!local_exists) { 113 | // TRYCASE(dir, nonexist) 114 | show_change(local_file, "created dir"); 115 | 116 | // don't traverse children, we copied the whole thing 117 | fts_set(fts, ent, FTS_SKIP); 118 | break; 119 | } 120 | 121 | // special "OPAQUE" whiteout directory--delete the original 122 | char xattr_buf[2] = { '\0', '\0' }; 123 | if (getxattr(ent->fts_path, "user.overlay.opaque", xattr_buf, 2) != -1 && xattr_buf[0] == 'y') { 124 | // TRYCASE(opaque, *) 125 | // TRYCASE(dir, dir) 126 | show_change(local_file, "replaced with dir"); 127 | break; 128 | } 129 | 130 | // non-directory replaced with a directory 131 | if (!S_ISDIR(local_stat.st_mode)) { 132 | // TRYCASE(dir, file) 133 | // TRYCASE(dir, symlink) 134 | show_change(local_file, "replaced with dir"); 135 | break; 136 | } 137 | 138 | // nothing of interest! directory got made, but modifications must be inside 139 | break; 140 | case FTS_F: // regular file 141 | if (getxattr(ent->fts_path, "user.overlay.whiteout", NULL, 0) != -1) { 142 | // TRYCASE(whiteout, *) 143 | show_change(local_file, "deleted"); 144 | break; 145 | } 146 | 147 | if (local_exists) { 148 | // TRYCASE(file, file) 149 | // TRYCASE(file, dir) 150 | // TRYCASE(file, symlink) 151 | show_change(local_file, "modified"); 152 | break; 153 | } 154 | 155 | // TRYCASE(file, nonexist) 156 | show_change(local_file, "added"); 157 | break; 158 | 159 | case FTS_SL: // symbolic link 160 | case FTS_SLNONE: // dangling symbolic link 161 | // TRYCASE(symlink, *) 162 | show_change(local_file, "symlink"); 163 | break; 164 | 165 | case FTS_DEFAULT: 166 | if (S_ISCHR(ent->fts_statp->st_mode) && ent->fts_statp->st_size == 0) { 167 | struct statx statxp; 168 | if (statx(AT_FDCWD, ent->fts_path, 0, STATX_TYPE | STATX_INO, &statxp) == -1) { 169 | fprintf(stderr, "try-summary: statx: %s: %s\n", ent->fts_path, strerror(errno)); 170 | break; 171 | } 172 | 173 | if (statxp.stx_rdev_major == 0 && statxp.stx_rdev_minor == 0) { 174 | // TRYCASE(whiteout, *) 175 | show_change(local_file, "deleted"); 176 | break; 177 | } 178 | } 179 | 180 | case FTS_DC: // cycle 181 | case FTS_DP: // postorder (second visit) 182 | break; 183 | 184 | case FTS_DNR: 185 | case FTS_ERR: 186 | default: 187 | fprintf(stderr, "try-summary: fts_read: %s: %s\n", ent->fts_path, strerror(errno)); 188 | break; // error with this directory 189 | } 190 | } 191 | 192 | if (errno != 0) { 193 | perror("try-summary: fts_read"); 194 | return 2; 195 | } 196 | 197 | fts_close(fts); 198 | free_ignores(); 199 | 200 | return changes_detected == 0 ? 1 : 0; 201 | } 202 | -------------------------------------------------------------------------------- /utils/version.h: -------------------------------------------------------------------------------- 1 | #ifndef __TRY_VERSION_H 2 | #define __TRY_VERSION_H 3 | 4 | #define TRY_VERSION "0.2.0" 5 | 6 | #endif 7 | --------------------------------------------------------------------------------