FocusedWolf

Arch: python.sh [auto venv activation and pyw support for terminal-less windows]

Jul 22nd, 2026 (edited)
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 9.04 KB | None | 0 0
  1. #!/bin/bash
  2.  
  3. # Version 24 — AI Enhanced™ Edition
  4.  
  5. # Unified Python launcher for Linux
  6. #
  7. # BEHAVIOR:
  8. #     *.pyw → run inline, open terminal only on error
  9. #     *.py  → always open terminal, run Python inside terminal
  10. #
  11. # FEATURES:
  12. #     - Auto activation of venv-installed libraries (script-local, cwd-local, global)
  13. #     - Correct error reporting to oh-my-posh (useful if your theme displays status codes), and terminal "Up arrow key" history will contain the command that ran the script.
  14. #     - rcfile auto-cleanup (no leftover /tmp files)
  15. #     - *.pyw script support (for GUI/Tkinter and headless scripts if ye prefer invisible ghost scripts running in the background)
  16. #
  17. # INSTALL:
  18. #     1. $ sudo cp ./python.sh /usr/local/bin/
  19. #     2. $ sudo chmod +x /usr/local/bin/python.sh
  20. #
  21. # USAGE:
  22. #     $ python.sh
  23. #     $ python.sh ./Script.py
  24. #     $ python.sh ./Script.pyw
  25.  
  26. # Dolphin Configuration:    <-- So you can double click .py and .pyw files to run them.
  27. #
  28. #     Open Dolphin, right-click any .py file > Properties > Open With > Click Change...
  29. #         Filename Patterns:
  30. #             Click "Add..."
  31. #             Extension: *.pyw
  32. #             Click OK
  33. #
  34. #         Application Preference Order:
  35. #             Click "Add..."
  36. #                 Click [Open file dialog] button > /usr/local/bin/python.sh
  37. #                 [x] Remember application association for all files of type "Python 3 script" (text/x-python3)
  38. #                 NOTE: "python.sh" needs to be the topmost item in the list for double clicked scripts to run with it.
  39. #                       If it isn't then click select "python.sh" and click "Move Up" until it is.
  40. #         Click Apply.
  41. #         Click OK.
  42.  
  43. # KDE Menu Editor Configuration:    <-- So you can create shortcuts to *.py and *.pyw scripts.
  44. #
  45. #     Click New > New Item...
  46. #     Name: <Script Name>
  47. #     Icon: <Path to icon.svg>
  48. #     Program: /usr/local/bin/python.sh
  49. #     Command-Line Arguments: <Path to script.py> or <Path to script.pyw>
  50. #     [ ] Enable launch feedback
  51.  
  52. # Offtopic notes for creating ~/.venv and installing python libraries there with pip:
  53. #
  54. #     Venv (virtual environment) creation:
  55. #         $ rm -r ~/.venv                  <-- Remove any existing venv folder at the destination.
  56. #         $ python3 -m venv ~/.venv        <-- Needs to be done on a linux partition (not ntfs apparently) or else "which pip" returns "/usr/bin/pip".
  57. #         $ source ~/.venv/bin/activate    <-- Activate the venv.
  58. #         $ which python                   <-- Verify python is ~/.venv/bin/python
  59. #         $ which pip                      <-- Verify pip is ~/.venv/bin/pip
  60. #
  61. #     Install Python libraries into the active .venv with pip:
  62. #         $ source ~/.venv/bin/activate    <-- Activate the venv.
  63. #         $ pip install keyboard pyttsx3
  64. #
  65. #     Making my scripts importable from other scripts:    <-- This new venv approach won't break when updating python versions.
  66. #         $ source ~/.venv/bin/activate    <-- Activate the venv.
  67. #         $ SITEDIR=$(python -c "import sys; print([p for p in sys.path if 'site-packages' in p][0])")    <-- NOTE: The typical command for this doesn't work with venv: $ SITEDIR=$(python -m site --user-site)
  68. #         $ mkdir -p "$SITEDIR"            <-- Create site directory if it doesn't exist.
  69. #         $ echo "/media/D/Users/Wolf/Files/Archives/Scripts/Python" >> "$SITEDIR/wolf-scripts.pth"
  70. #         $ python -m site                 <-- You should see the path you just added in this commands output.
  71. #
  72. #     Running python scripts from the terminal that rely on venv installed libraries:
  73. #         $ source ~/.venv/bin/activate
  74. #         $ python myscript.py
  75. #
  76. #         Or
  77. #
  78. #         $ python.sh myscript.py    <-- Relying on the script.
  79.  
  80. ###############################################
  81. # VENV RESOLUTION
  82. ###############################################
  83.  
  84. # Try three possible venv locations in priority order:
  85. # 1. Script directory's .venv
  86. # 2. Current directory's .venv
  87. # 3. Global ~/.venv
  88. for d in "$(dirname "$(readlink -f "$1" 2>/dev/null)")/.venv" "./.venv" "$HOME/.venv"; do
  89.    # If activate script exists, source it and stop searching.
  90.    [ -f "$d/bin/activate" ] && source "$d/bin/activate" && break
  91. done
  92.  
  93. ###############################################
  94. # TERMINAL DETECTION
  95. ###############################################
  96.  
  97. # Detect the first available terminal emulator.
  98. # Each terminal has its own syntax for executing a command.
  99. for t in kitty konsole alacritty wezterm gnome-terminal xfce4-terminal; do
  100.    if command -v "$t" &>/dev/null; then
  101.        case "$t" in
  102.            kitty|konsole|alacritty)
  103.                TERM=("$t" -e) ;;  # These use "-e <cmd>".
  104.            wezterm)
  105.                TERM=(wezterm start --) ;;  # WezTerm uses "start -- <cmd>".
  106.            gnome-terminal)
  107.                TERM=(gnome-terminal --) ;;  # GNOME Terminal uses "-- <cmd>".
  108.            xfce4-terminal)
  109.                TERM=(xfce4-terminal --execute) ;;  # XFCE Terminal uses "--execute <cmd>".
  110.        esac
  111.        break
  112.    fi
  113. done
  114.  
  115. # If no terminal found, abort.
  116. [ -z "$TERM" ] && echo "No terminal" && exit 1
  117.  
  118. ###############################################
  119. # NO ARGUMENTS → OPEN PYTHON REPL
  120. ###############################################
  121.  
  122. # Running the launcher with no arguments opens an interactive Python session.
  123. [ $# -eq 0 ] && exec "${TERM[@]}" python
  124.  
  125. ###############################################
  126. # SCRIPT PATH + ARGUMENT RESOLUTION
  127. ###############################################
  128.  
  129. # Resolve script path even if filename contains spaces.
  130. # We progressively join tokens until we find a valid file.
  131. _find() {
  132.    local a=("$@") n=${#a[@]}
  133.    for ((i=1;i<=n;i++)); do
  134.        local p="${a[*]:0:i}" # Join tokens 0..i-1 with spaces.
  135.        if [ -e "$p" ]; then
  136.            SCRIPT=$(readlink -f "$p") # Resolve absolute path.
  137.            ARGS=("${a[@]:i}") # Remaining tokens are arguments.
  138.            return
  139.        fi
  140.    done
  141.    # Fallback: treat first token as script.
  142.    SCRIPT=$(readlink -f "${a[0]}")
  143.    ARGS=("${a[@]:1}")
  144. }
  145. _find "$@"
  146.  
  147. # Quote arguments safely for shell injection.
  148. # This block is to prevent the creation of an '' empty string arg to the script.
  149. # printf '%q ' on a zero-length array still runs the format once and
  150. # quotes a "missing" arg as an empty string, i.e. it emits the literal
  151. # token '' instead of nothing. Left unguarded, that stray '' gets embedded
  152. # in the generated rc file / inline command line and is then parsed as a
  153. # genuine (spurious) empty-string argument to the Python script.
  154. # Guard it here so 0 args results in an empty ARGS_Q.
  155. if [ ${#ARGS[@]} -gt 0 ]; then
  156.    ARGS_Q="$(printf '%q ' "${ARGS[@]}")"
  157. else
  158.    ARGS_Q=""
  159. fi
  160.  
  161. ###############################################
  162. # .PYW → INLINE EXECUTION (HEADLESS)
  163. ###############################################
  164.  
  165. # Windows-style behavior: .pyw runs without opening a terminal, unless an error occurs.
  166. if [[ "${1,,}" == *.pyw ]]; then
  167.  
  168.    # Run Python inline and capture output + exit code.
  169.    out=$(PYTHON_COLORS=1 FORCE_COLOR=1 python -- "$SCRIPT" $ARGS_Q 2>&1)
  170.    st=$?
  171.  
  172.    # If error, open terminal to display error and drop into prompt.
  173.    if (( st != 0 )); then
  174.  
  175.        # Create temporary rcfile for bash --init-file.
  176.        rc=$(mktemp)
  177.  
  178.        cat > "$rc" <<EOF
  179. # Source user's shell configuration.
  180. [ -f ~/.bashrc ] && source ~/.bashrc
  181.  
  182. # Print Python error output + blank line.
  183. printf "%s\n\n" "$out"
  184.  
  185. # Export exit code for oh-my-posh.
  186. export PROMPT_LAST_EXIT=$st
  187.  
  188. # Insert clean history entry (no stray '' on the end).
  189. if [ ${#ARGS[@]} -gt 0 ]; then
  190.    history -s "python -- \"$SCRIPT\" $ARGS_Q"
  191. else
  192.    history -s "python -- \"$SCRIPT\""
  193. fi
  194.  
  195. # Delete this rcfile now that it has finished executing.
  196. rm -f "\$BASH_SOURCE" >/dev/null 2>&1 || true
  197.  
  198. # Exit with Python's exit code so oh-my-posh detects failure.
  199. (exit $st)
  200. EOF
  201.  
  202.        # Open terminal running the rcfile.
  203.        exec "${TERM[@]}" bash --init-file "$rc"
  204.    fi
  205.  
  206.    # Inline execution exit code.
  207.    exit $st
  208. fi
  209.  
  210. ###############################################
  211. # .PY → ALWAYS OPEN TERMINAL
  212. ###############################################
  213.  
  214. # Windows-style behavior: .py always opens a console window.
  215. rc=$(mktemp)
  216.  
  217. cat > "$rc" <<EOF
  218. # Source user's shell configuration.
  219. [ -f ~/.bashrc ] && source ~/.bashrc
  220.  
  221. # Run Python inside terminal (streams output live).
  222. PYTHON_COLORS=1 FORCE_COLOR=1 python -- "$SCRIPT" $ARGS_Q
  223. st=\$?
  224.  
  225. # Always add a blank line after Python output.
  226. echo
  227.  
  228. # Export exit code for oh-my-posh.
  229. export PROMPT_LAST_EXIT=\$st
  230.  
  231. # Insert clean history entry (no stray '' on the end).
  232. if [ ${#ARGS[@]} -gt 0 ]; then
  233.    history -s "python -- \"$SCRIPT\" $ARGS_Q"
  234. else
  235.    history -s "python -- \"$SCRIPT\""
  236. fi
  237.  
  238. # Delete this rcfile now that it has finished executing.
  239. rm -f "\$BASH_SOURCE" >/dev/null 2>&1 || true
  240.  
  241. # Exit with Python's exit code so oh-my-posh detects failure/success.
  242. (exit \$st)
  243. EOF
  244.  
  245. # Launch terminal running the rcfile.
  246. exec "${TERM[@]}" bash --init-file "$rc"
Advertisement
Add Comment
Please, Sign In to add comment