├── VERSION ├── LICENSE ├── README.md └── shell /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.5 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Joris Verschoor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shell (container-shell) v0.2.5 2 | 3 | Starts and attaches a sandboxed shell using docker with access to the current or project directory. 4 | It could also be described as chroot mixed with containers. 5 | 6 | Please visit https://github.com/jrz/container-shell for the latest version. 7 | 8 | 9 | # Why? 10 | I like using containers to keep a project's dependencies and effects contained. 11 | 12 | Sometimes I simply want to have a sandboxed shell when using or testing a tool. 13 | 14 | With the recent supplychain attacks this has become more important to me. 15 | 16 | 17 | 18 | # How does it work? 19 | In essence, it starts a container with a keep-alive loop which runs until no terminals are connected by checking the number of /dev/pty/*. 20 | 21 | After it has created the container, it immediately starts a shell for it. 22 | 23 | The container id is stored in .docker.shell.cid in order to re-connect subsequent shells. 24 | If you start another shell, it will first determine if it needs to start/create the container. 25 | 26 | It can read a Shellfile in a project directory to configure things like the docker image. 27 | In order to find the Shellfile or .docker.shell.cid it checks the current dir and all parents. 28 | 29 | 30 | 31 | # Installation 32 | There is no installer or package yet. Simply copy the shell script to any directory in your PATH. 33 | Make sure that the executable bit is set. For example: 34 | ``` 35 | cp shell /usr/local/bin/ 36 | chmod +x /usr/local/bin/shell 37 | ``` 38 | 39 | # Updating 40 | To check for the latest version, use the following command: 41 | ``` 42 | shell update 43 | ``` 44 | 45 | 46 | # Usage 47 | The CLI will most likely change. The program can be used ad-hoc or using a Shellfile: 48 | 49 | ## Ad hoc (no Shellfile) 50 | ``` 51 | > shell [docker-image] 52 | ``` 53 | For example `shell debian:trixie` 54 | 55 | ## Using a Shellfile 56 | Use the ```shell init``` command to create a new Shellfile in the current directory if no other Shellfile can be found. The Shellfile will be sourced to override default settings. Please refer to the 'Shellfile settings' section for all available settings. 57 | 58 | ``` 59 | > shell init # Creates a Shellfile with a debian:trixie image 60 | > vi ./Shellfile # Optionally edit the Shellfile 61 | > shell # Run the container 62 | ``` 63 | 64 | 65 | 66 | ## Shellfile settings 67 | Example: 68 | ``` 69 | # Docker image to use when creating the container (debian:trixie) 70 | DOCKER_IMAGE="debian:trixie" 71 | 72 | # Naming prefix (ex. shellcontainer-myproject) 73 | DOCKER_PREFIX="sc-myproject" 74 | 75 | # Passed when creating container (ex. --platform linux/aarch64) 76 | DOCKER_OPTS="--platform linux/aarch64" 77 | 78 | # Temporary terminal background color when inside container (#011279) 79 | TERM_BG_COLOR="#011279" 80 | 81 | # Temporary iTerm2 profile name when inside container (container-shell) 82 | MOUNTPOINT="/project" 83 | ``` 84 | 85 | 86 | 87 | # Misc 88 | I know this is not how docker is intended to work, but this is what I want/need. 89 | 90 | It uses docker for now, but could also support nixos via OrbStack 'machines' in the future. 91 | See https://github.com/orbstack/orbstack/issues/169 92 | 93 | To use the ITERM2_PROFILE setting instead of using only background colors, create a new profile in iTerm2 with the name "shell-container" (or something else). Change its settings such as background color. 94 | 95 | The bindmount fails when the project directory is moved after creation. 96 | 97 | 98 | # Ideas 99 | - It'd be nice to be able to automatically build a Dockerfile when it's in the current directory. 100 | - Add rm command to clean up containers. 101 | - Make the default domain compatible with other things than OrbStack 102 | - Add PACKAGE_LIST to Shellfile to automatically build a derived docker image 103 | - Add an interactive ui for ```shell init``` 104 | -------------------------------------------------------------------------------- /shell: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # (c) 2023 Joris Verschoor 3 | # Starts and attaches a sandboxed shell using docker (OrbStack) 4 | # See https://github.com/jrz/container-shell 5 | 6 | set -e 7 | 8 | VERSION="0.2.5" 9 | GIT_REPO="jrz/container-shell" 10 | 11 | print_usage() { 12 | cat < | update] 15 | 16 | Starts and attaches a sandboxed shell using docker. 17 | 18 | The current or project directory is bind-mounted. 19 | The container is reused for shells in the project directory. 20 | The container will stop automatically when there are no sessions anymore. 21 | See https://github.com/jrz/container-shell for limitations and how it works. 22 | 23 | ** CLI and settings will probably change in the future ** 24 | ** Please do not use for scripting (yet) ** 25 | 26 | Examples 27 | shell init Creates a new Shellfile with linux container 28 | 29 | shell Creates a new shell based on an existing Shellfile 30 | shell debian:trixie Creates a new ad-hoc shell based on debian:trixie 31 | 32 | Both commands check if a container exists using a .docker.shell.cid file. 33 | Both commands will result in an attached shell. 34 | 35 | shell update Checks if a newer version available of this tool 36 | 37 | 38 | Options 39 | -f Name of the Shellfile (default: Shellfile) 40 | -n Don't traverse directory tree / create new ad-hoc instance 41 | -h, --help Show this message 42 | 43 | Shellfile / Environment variables 44 | DOCKER_IMAGE Docker image to use when creating the container (debian:trixie) 45 | DOCKER_PREFIX Naming prefix (ex. shellcontainer-myproject) 46 | DOCKER_MOUNTPOINT Where the project root is mounted in the container 47 | (defaults to mirroring the host's location) 48 | DOCKER_OPTS Passed when creating container (ex. --platform linux/aarch64) 49 | TERM_BG_COLOR Temporary terminal background color when inside container (#011279) 50 | ITERM2_PROFILE Temporary iTerm2 profile name when inside container (container-shell) 51 | EOF 52 | } 53 | 54 | print_short_usage() { 55 | cat </dev/null)" 116 | latest_exitcode=$? 117 | set -e 118 | if [ "$latest_exitcode" != "0" ]; then 119 | echo "Error checking latest version at $latest_url" 120 | exit 1 121 | fi 122 | 123 | echo "Latest version: $latest (you have $VERSION)" 124 | 125 | if [ -n "$latest" ] && [ "$latest" != "$VERSION" ]; then 126 | echo 127 | echo " New version available. Update with: " 128 | shell_script_path=$(realpath "$0") 129 | echo " curl -fsSL https://raw.githubusercontent.com/$GIT_REPO/main/shell -o $shell_script_path" 130 | fi 131 | } 132 | 133 | 134 | ARGS_COMMAND="$1" 135 | if [ "$ARGS_COMMAND" ]; then 136 | if [ "$ARGS_COMMAND" == "update" ]; then 137 | check_for_update 138 | exit 0 139 | elif [ "$ARGS_COMMAND" == "init" ]; then 140 | : # do nothing yet 141 | else 142 | ARGS_DOCKER_IMAGE="$ARGS_COMMAND" 143 | unset ARGS_COMMAND 144 | fi 145 | fi 146 | 147 | 148 | 149 | # Searches for the project root containing Shellfile or cidfile 150 | # Starts in the current directory, and keeps going up one level. 151 | find_project_root() { 152 | set -e 153 | file_to_look_for=$(basename "$1") 154 | parent=$(dirname "$file_to_look_for") 155 | parent=$(realpath "$parent") 156 | found="" 157 | done="" 158 | while [ ! "$found" ] && [ ! "$done" ]; do 159 | if [ "$parent" = "/" ]; then 160 | done=true 161 | fi 162 | 163 | # Shellfile 164 | if [ -f "${parent}/$file_to_look_for" ]; then 165 | found=${parent} 166 | fi 167 | 168 | parent=$(dirname "$parent") 169 | done 170 | if [ "$found" ]; then 171 | echo "$found" 172 | fi 173 | # otherwise return empty. 174 | } 175 | 176 | 177 | 178 | 179 | 180 | # Settings 181 | SHELLFILE=${OPT_SHELLFILE:-./Shellfile} 182 | CID_FILE=".docker.shell.cid" 183 | 184 | # Find containing id in current directory or any parent dir 185 | if [ "$DONT_TRAVERSE_PROJECT_ROOT" ]; then 186 | PROJECT_ROOT=$(pwd) 187 | else 188 | PROJECT_ROOT=$(find_project_root "$SHELLFILE") 189 | fi 190 | 191 | PROJECT_ROOT=$(realpath "${PROJECT_ROOT:-.}") 192 | 193 | 194 | # TODO: check for conflicting opt_shell file and project root 195 | # If we have detected an existing project root 196 | if [ "$PROJECT_ROOT" ]; then 197 | if [ ! "$OPT_SHELLFILE" ]; then 198 | if [ -f "$PROJECT_ROOT/Shellfile" ]; then 199 | SHELLFILE="${PROJECT_ROOT}/Shellfile" 200 | fi 201 | fi 202 | 203 | if [ -f "$PROJECT_ROOT/$CID_FILE" ]; then 204 | CID_FILE="$PROJECT_ROOT/$CID_FILE" 205 | fi 206 | else 207 | PROJECT_ROOT=$(realpath "`pwd`") 208 | fi 209 | 210 | 211 | 212 | 213 | if [ "$ARGS_COMMAND" = "init" ]; then 214 | # init local file if it does not exist. 215 | if [ -f "$SHELLFILE" ]; then 216 | echo "Shellfile already exists at $SHELLFILE" 217 | exit 1 218 | else 219 | echo "Creating new Shellfile at $SHELLFILE" 220 | touch $SHELLFILE 221 | echo 'DOCKER_IMAGE="debian:trixie"' >> $SHELLFILE 222 | 223 | echo "Please review the Shellfile and start the container with:" 224 | echo "" 225 | echo " shell (no arguments)" 226 | exit 0 227 | fi 228 | fi 229 | 230 | if [ "$ARGS_DOCKER_IMAGE" ]; then 231 | ARGS_DOCKER_IMAGE="$1" 232 | elif [ -f "$CID_FILE" ]; then 233 | # We have a cid-file, so we can start without arguments 234 | : # do nothing 235 | else 236 | if ! [ -f "$SHELLFILE" ]; then 237 | # 238 | echo "$SHELLFILE does not exist and no cid-file exists. Please run 'shell init' or specify an ad-hoc docker-image." 239 | echo "" 240 | print_short_usage 241 | 242 | exit 1 243 | fi 244 | fi 245 | 246 | 247 | # Defaults 248 | # use the project directory as a a prefix 249 | PROJECT_NAME=$(basename "$PROJECT_ROOT" | tr ' ' '-' | tr -dc '[:alnum:]-' | tr '[:upper:]' '[:lower:]') 250 | DOCKER_IMAGE="debian:trixie" 251 | DOCKER_PREFIX="shellcontainer-$PROJECT_NAME" 252 | # DOCKER_OPTS="--platform linux/aarch64" 253 | # Empty in case the profile doesn't exist 254 | ITERM2_PROFILE="" 255 | TERM_BG_COLOR="#011279" 256 | # Use the project root as the mountpoint in the container to match the host 257 | MOUNTPOINT="${DOCKER_MOUNTPOINT:-$PROJECT_ROOT}" 258 | 259 | # TODO: What is the default docker domain? 260 | CT_DOMAIN=.local 261 | # OrbStack uses .orb.local hostnames 262 | if [ "$(command -v orb)" ]; then 263 | CT_DOMAIN=".orb.local" 264 | fi 265 | 266 | # Load user defaults? 267 | [ -f "$HOME/.config/Shellfile.defaults" ] && . "$HOME/.config/Shellfile.defaults" 268 | 269 | # Load current shellfile settings 270 | [ -f "$SHELLFILE" ] && . "$SHELLFILE" 271 | 272 | # TODO: Check if it matches the Shellfile, or cidfiles / inspect? 273 | if [ "$ARGS_DOCKER_IMAGE" ]; then 274 | DOCKER_IMAGE="$ARGS_DOCKER_IMAGE" 275 | fi 276 | 277 | 278 | # Split #rrggbb 279 | TERM_BG_COLOR_R=${TERM_BG_COLOR:1:2} 280 | TERM_BG_COLOR_G=${TERM_BG_COLOR:3:2} 281 | TERM_BG_COLOR_B=${TERM_BG_COLOR:5:2} 282 | 283 | 284 | ################## 285 | # This is the keep-alive script to run as an entry point 286 | # It loops until there are no pseudo-TTY connected. 287 | # It periodically checks /dev/pts. 288 | # When the loop exits, the container will stop. 289 | keep_alive_cmd=$(cat < In POSIX sh, brace expansion is undefined. 319 | # /dev/urandom is our best best compared to $RANDOM or shuf 320 | random_word=$(LC_ALL=C tr -dc 'a-z' < /dev/urandom | head -c8; echo) 321 | fi 322 | 323 | # Name and hostname 324 | ct_name="${DOCKER_PREFIX}-${random_word}" 325 | ct_hostname="${ct_name}${CT_DOMAIN}" 326 | 327 | # Start new container, bind current directory, and start keep alive loop 328 | # TODO: How to pass DOCKER_OPTS properly? 329 | echo "Starting new container: $ct_hostname from $DOCKER_IMAGE" 330 | docker run -h "$ct_hostname" --name "$ct_name" --cidfile "$CID_FILE" -id --mount type=bind,target="$MOUNTPOINT",source="$(pwd)" $DOCKER_OPTS "$DOCKER_IMAGE" /bin/sh -c "$keep_alive_cmd" 331 | 332 | CID=$(cat "$CID_FILE") 333 | fi 334 | 335 | 336 | 337 | 338 | ################## 339 | # Print project root if not the current directory 340 | if [ "$PROJECT_ROOT" != "$(pwd | realpath "`pwd`")" ]; then 341 | echo "Project root: $PROJECT_ROOT" 342 | fi 343 | 344 | ################## 345 | # (Re)start or attach to container, print info and cd into the relative directory 346 | CID=$(cat "$CID_FILE") 347 | SHORT_CID=$(echo "$CID" | head -c 16) 348 | 349 | # (re)start if stopped 350 | if [ ! "$(docker ps -q -f id="$CID")" ]; then 351 | echo "Restarting: $SHORT_CID" 352 | docker start "$CID" 353 | else 354 | echo "Attaching to: $SHORT_CID" 355 | fi 356 | 357 | ################## 358 | # Warning if the image differs from the setting 359 | CONTAINER_IMAGE=$(docker container inspect -f "{{ .Config.Image }}" "$CID") 360 | if [ ! "$CONTAINER_IMAGE" = "$DOCKER_IMAGE" ]; then 361 | echo 362 | echo "Warning: Current container is based on ${CONTAINER_IMAGE} while ${DOCKER_IMAGE} is specified." 363 | 364 | if [ "$ARGS_DOCKER_IMAGE" ]; then 365 | # Adhoc 366 | echo "Multiple containers in one project/directory is currently not supported." 367 | echo "Please delete the .docker.shell.cid file (container $SHORT_CID)." 368 | echo "Note: Doing so will create a new container on the next run." 369 | else 370 | # Using shellfile 371 | echo "Please update the Shellfile to use to original image ($CONTAINER_IMAGE)" 372 | echo "or delete the .docker.shell.cid file (container $SHORT_CID)." 373 | echo "Note: Doing so will create a new container on the next run." 374 | fi 375 | echo 376 | fi 377 | 378 | 379 | ################## 380 | # Print URLs 381 | # Main network: fmt="http://{{.Config.Hostname}} or http://{{.NetworkSettings.IPAddress }}" 382 | fmt="http://{{.Config.Hostname}}{{ range .NetworkSettings.Networks }} or http://{{ .IPAddress }}{{end}}" 383 | CONTAINER_URLS=$(docker container inspect -f "$fmt" "$CID") 384 | echo "URLs: $CONTAINER_URLS" 385 | 386 | 387 | 388 | 389 | ################## 390 | # Check directory relative to directory of .docker.shell.cid 391 | # Does not work with multiple mountpoints. 392 | # We cannot use realpath --relative-to. 393 | # realpath --relative-to="$CONTAINER_MOUNT_HOST" "$(pwd)" 394 | CONTAINER_MOUNT_HOST=$(docker container inspect -f "{{ range .HostConfig.Mounts }}{{.Source}}{{ end }}" "$CID") 395 | MOUNTPOINT=$(docker container inspect -f "{{ range .HostConfig.Mounts }}{{.Target}}{{ end }}" "$CID") 396 | 397 | CONTAINER_MOUNT_HOST=$(realpath "$CONTAINER_MOUNT_HOST") 398 | HOST_PWD=$(realpath "`pwd`") 399 | 400 | # concatenate mountpoint + relative path 401 | # TODO: Find a posix compatible way to find/strip the path 402 | CONTAINER_WORKDIR="${MOUNTPOINT}${HOST_PWD/#$CONTAINER_MOUNT_HOST}" 403 | 404 | 405 | 406 | 407 | ################## 408 | # Wrap the command in different iterm2 profile 409 | # This uses some control characters to change the iTerm2 profile 410 | # It also resets (clears) the profile after the child command exits 411 | set_term_bg_color() { 412 | # hex does not work everywhere "\x1b]11;$TERM_BG_COLOR\x1b\\" 413 | printf "\x1b]11;rgb:$TERM_BG_COLOR_R/$TERM_BG_COLOR_G/$TERM_BG_COLOR_B\x1b\\" 414 | } 415 | reset_term_bg_color() { 416 | printf "\x1b]111;\x1b\\" 417 | } 418 | 419 | if [ "$TERM_PROGRAM" = "iTerm.app" ]; then 420 | # For iterm, we can use profile names instead of colors 421 | if [ "$ITERM2_PROFILE" ]; then 422 | set_term_bg_color() { 423 | # First argument is the template name 424 | printf "\e]1337;SetProfile=$ITERM2_PROFILE\a" 425 | } 426 | reset_term_bg_color() { 427 | printf "\e]1337;SetProfile=\a" 428 | } 429 | fi 430 | elif [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then 431 | : # Works by default 432 | elif [ "$TERM_PROGRAM" = "vscode" ]; then 433 | : # Works by default 434 | elif [ "$TERM_PROGRAM" = "Alacritty" ] || [ "$ALACRITTY_WINDOW_ID" ]; then 435 | : # Works by default, TERM_PROGRAM not supported yet, doesn't matter 436 | elif [ "$ZED_TERM" = "true" ]; then 437 | : # TODO: Zed doesn't respond to OSC 11. TERM_PROGRAM not supported yet. 438 | fi 439 | 440 | 441 | ################## 442 | # Set terminal colors and trap to reset them again 443 | trap reset_term_bg_color EXIT HUP INT QUIT PIPE TERM 444 | set_term_bg_color 445 | 446 | 447 | 448 | ################## 449 | # Attach to container using the user's shell and cd into the relative directory 450 | # Ex: docker exec -it -w /media $CID /bin/sh -c /bin/bash 451 | 452 | 453 | # This command is passed to the initial /bin/sh, and switched to the user's shell instead of /bin/sh 454 | # Note: Does not read any passwords. This is done to find the shell of the user (inside the container) 455 | # "getent passwd " to query NSS (ldap/ad/etc) (inside the container) 456 | # /etc/passwd to check local configuration (inside the container) 457 | exec_user_shell_cmd=$(cat </dev/null 2>&1; then 462 | user_shell=\$(getent passwd "\$user_name" | awk -F: '{print \$7}') 463 | fi 464 | 465 | # Fallback to passwd file 466 | if [ -z "\$user_shell" ] && [ -r /etc/passwd ]; then 467 | user_shell=\$(awk -F: -v u="\$user_name" '\$1 == u { print \$7; exit }' /etc/passwd) 468 | fi 469 | 470 | # fall back 471 | [ -n "\$user_shell" ] || user_shell=/bin/sh 472 | 473 | # replace current process, force interactive shell 474 | exec \$user_shell -i 475 | EOF 476 | ) 477 | 478 | 479 | docker exec --interactive --tty --workdir "$CONTAINER_WORKDIR" "$CID" /bin/sh -ic "$exec_user_shell_cmd" 480 | 481 | --------------------------------------------------------------------------------