Repository: xonsh/xonsh
Pull request: Fix jobs.py: list index out of range
Fixes https://github.com/xonsh/xonsh/issues/2544#issuecomment-2059957967

Closes #2544

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**


Problem statement:
"command&" misparsed
`true&` is misparsed. It should execute `true` in the background, but instead treats the whole thing as the command name.

`true &` works fine.

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**

Hints from issue discussion:
Thanks for reporting!
I don't think that this is easily fixable, since one of the ways that the parser works is that it says whitespace counts in subprocess mode. Addressing this would special case backgrounding to have whitespace not matter for the last token. That said, we probably would still want this....
This also affects `bash -c 'sleep 5 &'` invocation. Example:

> `xonsh $` indicates a xonsh prompt
> `bash $` indicates a bash prompt

``` console
xonsh $ import time; start = time.time(); bash -c 'sleep 5 &'; print(time.time() - start)
5.01800537109375

bash $ time bash -c 'sleep 5 &'
real    0m0.014s
user    0m0.008s
sys     0m0.005s
```

This make tools that use background jobs unusable in the xonsh. For example `pass -c something` (https://www.passwordstore.org/) just blocks the shell(until password is not deleted from clipboard).
Hi @corpix - I think bash is doing something very strange here.... I am not sure that is the correct behaviour to replicate.
Though the pass case is a notable usecase.
I'm having similar behavior with `nvim-qt`; instead of backgrounding like it does in other shells, it blocks the `xonsh` session until I exit `nvim-qt`.

Seems like that's probably a separate issue from this one... if I don't find an existing issue for it, I'll open one.
Hi @whitelynx - that seems like a separate issue. Would you mind opening up another issue please?
Actually, it looks like my issue is a duplicate of #2060... I'll try the hack from that issue.
On current master I can repeat this with more errors:
```xsh
echo&
```
```xsh
TRACE SUBPROC: (['echo'], '&'), captured=hiddenobject
xonsh: To log full traceback to a file set: $XONSH_TRACEBACK_LOGFILE = <filename>
Traceback (most recent call last):
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/specs.py", line 474, in _run_binary
    p = self.cls(cmd, bufsize=bufsize, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/subprocess.py", line 1026, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/subprocess.py", line 1955, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'echo'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/pipelines.py", line 167, in __init__
    proc = spec.run(pipeline_group=pipeline_group)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/specs.py", line 456, in run
    p = self._run_binary(kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/specs.py", line 493, in _run_binary
    raise xt.XonshError(e) from ex
xonsh.tools.XonshError: xonsh: subprocess mode: command not found: 'echo'
xonsh: To log full traceback to a file set: $XONSH_TRACEBACK_LOGFILE = <filename>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/built_ins.py", line 206, in subproc_captured_hiddenobject
    return xonsh.procs.specs.run_subproc(cmds, captured="hiddenobject", envs=envs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/specs.py", line 908, in run_subproc
    return _run_specs(specs, cmds)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/procs/specs.py", line 922, in _run_specs
    xj.add_job(
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/jobs.py", line 376, in add_job
    print_one_job(num)
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/jobs.py", line 354, in print_one_job
    info = format_job_string(num)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/pc/.local/mamba-env/Users/pc/git/trace/lib/python3.12/site-packages/xonsh/jobs.py", line 347, in format_job_string
    pid = job["pids"][-1]
          ~~~~~~~~~~~^^^^
IndexError: list index out of range
```
I fixed the exception. Other cases works fine:
```xsh
true&
# [1]+ running: true &
true &
# [1]+ running: true &

qwe&
# xonsh: subprocess mode: command not found: 'qwe'
# [1]+ running: qwe &
qwe &
# xonsh: subprocess mode: command not found: 'qwe'
# [1]+ running: qwe &

aliases['qqq']='echo 123'
qqq&
# [1]+ running: qqq &
# 123
qqq &
# [1]+ running: qqq &
# 123
```

Commit message: test: update venv activator test

Patch to review:
diff --git a/news/fix-jobs-index.rst b/news/fix-jobs-index.rst
new file mode 100644
index 0000000000..9f735037bc
--- /dev/null
+++ b/news/fix-jobs-index.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Jobs: fixed "index out of range" exception.
+
+**Security:**
+
+* <news item>
diff --git a/tests/test_virtualenv_activator.py b/tests/test_virtualenv_activator.py
index 345e7969de..96d490cd1a 100644
--- a/tests/test_virtualenv_activator.py
+++ b/tests/test_virtualenv_activator.py
@@ -8,9 +8,7 @@
 def test_xonsh_activator(tmp_path):
     # Create virtualenv
     venv_dir = tmp_path / "venv"
-    assert b"XonshActivator" in check_output(
-        [sys.executable, "-m", "virtualenv", str(venv_dir)]
-    )
+    check_output([sys.executable, "-m", "venv", str(venv_dir)])
     assert venv_dir.is_dir()
 
     # Check activation script created
diff --git a/xonsh/jobs.py b/xonsh/jobs.py
index 097346a1b0..652967500b 100644
--- a/xonsh/jobs.py
+++ b/xonsh/jobs.py
@@ -344,9 +344,9 @@ def format_job_string(num: int) -> str:
     pos = "+" if tasks[0] == num else "-" if tasks[1] == num else " "
     status = job["status"]
     cmd = " ".join([" ".join(i) if isinstance(i, list) else i for i in job["cmds"]])
-    pid = job["pids"][-1]
+    pid = f"({job['pids'][-1]})" if job["pids"] else ""
     bg = " &" if job["bg"] else ""
-    return f"[{num}]{pos} {status}: {cmd}{bg} ({pid})"
+    return f"[{num}]{pos} {status}: {cmd}{bg} {pid}"
 
 
 def print_one_job(num, outfile=sys.stdout):


Code review comments:
diff_hunk: @@ -8,9 +8,7 @@
 def test_xonsh_activator(tmp_path):
     # Create virtualenv
     venv_dir = tmp_path / "venv"
-    assert b"XonshActivator" in check_output(
-        [sys.executable, "-m", "virtualenv", str(venv_dir)]
-    )
+    check_output([sys.executable, "-m", "venv", str(venv_dir)])
path: tests/test_virtualenv_activator.py
text: @user1:
I am checking this

@user1:
@user2-code @user3 this module better be extracted to its own plugin like suggested by the maintainer https://github.com/xonsh/xonsh/issues/3689 . The plugin doesn't need to depend on Xonsh at all. It can be a pure Python file as well. 

@user1:
Also there is a recent release from virtualenv which could have broken this test. https://pypi.org/project/virtualenv/20.25.2/ . Also this is not a stdlib and we have vox for supporting venvs . So I suggest moving this code to a separate project.