0

If I run following Python script on Linux Mint 21 Cinnamon edition with Python 3.11.2:

import os
T = "gnome-terminal"
os.system(f'{T} -- python -ic"import os; os.system(\'resize -s 5 32\')"')
os.system(f'{T} -- bash   -ic "resize -s 5 32"')

it creates following two Terminal windows:

enter image description here


UPDATE as reaction to a comment by Cyrus requesting to provide text instead of an image: if I replace the image with the text it would give in case of the first window:

COLUMNS=32;
LINES=5;
export COLUMNS LINES;
>>> 

and in the case of the second window:

The child process exited normally with status 0.

making it as good as not possible to directly see what the question is about. Images are sometimes the much better option.


Notice that both of the script lines are doing the same job and are using the same command line options. The line using Python does not require a space between -ic and the string. The line with bash does. In this context a question:

What is wrong with my expectation that both lines should give an interactive Terminal window?

The first line results as expected in an interactive Python session, but the second line fails to give an interactive session and wouldn't rise a Terminal window at all when in the Terminal settings the option to stay opened after the child process exits wouldn't be checked.

I would be glad if someone in course of giving an answer to my question could also explains why the line with bash needs the space where the Python line doesn't and how to tweak the code creating the interactive Python session so that it will fall back to a bash shell prompt instead of exiting the Terminal window if Python exits.

UPDATE as reaction to comment by Charles Duffy:

The info bash command gives as an explanation of the -i option for bash: -i If the -i option is present, the shell is interactive. and man bash gives the same explanation. Interactive when not running??? Has the word interactive another meaning? Which one?

After reading the details of the bash -i option as suggested in the comments it turns out that the word interactive is severe misused here and means the opposite of what I would expect from it. So if I want the shell to respond to the provided code I have to run the shell NON-interactive. OK. In this context it seems that my actual question should be:

> If your real question is ("how do I get bash to run some arbitrary code and then drop to an interactive shell?", then maybe ask that directly. – Charles Duffy

Below my attempt to use subprocess:

from subprocess import run
run([f'{T}', '--', 'python', '-ic', '"import os; os.system(\'resize -s 5 32\')"'])
run([f'{T}', ' -- bash ', '-i' , '-c', '"resize -s 5 32"'])

The code above does not resize the first window and gives following Error message on the second one:

# Failed to parse arguments: Unknown option -i

Corrected the code for the second window to:

run([f'{T}', '--', 'bash ' , '-i' , '-c', '"resize -s 5 32"'])

Now both windows are created without resize and the second one gives:

# Error: Failed to execute child process “bash ”: Failed to execve: No such file or directory
Claudio
  • 7,474
  • 3
  • 18
  • 48
  • 1
    Please replace image with its text. – Cyrus Mar 03 '23 at 18:27
  • 1
    `bash -i` tells bash to run with the interactive flag set, which changes its behavior in all the ways that the bash manual tells you it does. It has nothing to do with the `python -i` meaning of "drop to an interactive prompt when done with the script", though, so the behavior you're seeing is 100% correct and as-documented. – Charles Duffy Mar 03 '23 at 18:33
  • If your real question is "how do I get bash to run some arbitrary code and then drop to an interactive shell?", then maybe ask that directly. – Charles Duffy Mar 03 '23 at 18:34
  • 1
    (as for "why the bash line needs the space and the Python line doesn't" -- they're different programs with different command-line parsers; every program implements its own parsing; the only standards things are expected to hew to are those given in section 12.2 of https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html, and Guideline 6 there clearly states that each option-argument is expected to be a separate command-line argument, so the way POSIX requires tools to support is _with the space_; supporting leaving out the space is a nonstandard extension). – Charles Duffy Mar 03 '23 at 18:37
  • 1
    BTW, using `os.system()` _at all_ is a pretty significant code smell; it creates an extra copy of `/bin/sh`, with all the extra complexities that come from needing an extra layer of escaping on top of your command (instead of being able to just pass the argument vector directly). Much better to stick to `subprocess`-family functions with the default `shell=False` wherever possible. – Charles Duffy Mar 03 '23 at 18:40
  • Read the man page more thoroughly. `man bash` tells you _in detail_ all the ways an interactive shell differs from a noninteractive one, and those are the _only_ ways it's different. Think about things like running `~/.bashrc`, or turning history expansion on by default. Anything that isn't in that documentation -- like trying to read code from stdin -- it doesn't do. – Charles Duffy Mar 03 '23 at 18:52
  • Seriously, just search the man page; it's full of the phrase "when the shell is interactive" or "if the shell is interactive". You'll also see a lot of things talking about "[Aa] non-interactive shell", "an interactive shell", etc. **To answer this question in the level of detail requested would just mean collating all those things from the man page, which is a lot more work than it would be to just ask you to search the manual yourself**, which would also ensure that you're getting an answer that's accurate for the specific version of bash that you're running. – Charles Duffy Mar 03 '23 at 18:53
  • (Correspondingly, I've voted to close this as too broad to be on-topic as currently asked) – Charles Duffy Mar 03 '23 at 18:55
  • So, yes, "interactive" has a _lot_ of meaning other than the intuitive one, _and the manual describes that meaning in excruciating detail_; far too much detail to fit in a reasonable SO answer. – Charles Duffy Mar 03 '23 at 18:57
  • Whereas insofar as your _real_ question is "How do I make bash run an arbitrary command and then hand control over to the user?", that's a duplicate of [execute command but continue with interactive session](https://stackoverflow.com/questions/4584736/bash-execute-command-but-continue-with-interactive-session) – Charles Duffy Mar 03 '23 at 18:58
  • 1
    The link given to the "duplicate" does not explain much to me, except: *"you do not understand what they are speaking about? Just read more manuals and pay much more attention to detail."*. The advice to get the question answered by myself because a good answer would be to much work isn't really helpful here. I am already some workdays busy with learning about it asking for help before giving up on it as too much effort for such a small step. **If it is so much effort to explain it ... maybe it is the wrong path to go???** and I should better give up on that path? – Claudio Mar 03 '23 at 19:22
  • The simplest answer to how do I get bash to execute code in an interactive session trespassing all that details which are to much to explain would be: start the Terminal from Python then "use pyautogui" to run python within it ... Wouldn't it be a much simpler and better path? Working in any case for any shell in any environment??? – Claudio Mar 03 '23 at 19:31
  • I gave subprocess a try ... running into more trouble without getting any expected behavior at all ... – Claudio Mar 03 '23 at 19:37
  • I don't understand what is to do. HOW can I use --rcfile? Just put it as an option? Without the file? I will try it out ... – Claudio Mar 03 '23 at 21:06
  • I added an answer of my own (well, not really "of my own" -- community-wiki, ownership-disclaimed, because I won't stand behind an answer to an off-topic question), but **the duplicate has an answer that shows you how to use `--rcfile` already**. Granted, it shows how to use it _from bash_ instead of _from Python_, so if you want to use the exact same practice you also need to find and follow instructions from another existing Q&A entry like [bash-style process substitution with Python's `Popen`](https://stackoverflow.com/questions/15343447); but using a temporary file moots the need. – Charles Duffy Mar 03 '23 at 21:08
  • I don't even understand what the question is about in the link you suggested to study, not mention that the answer is even more unreadable to me. It's a mess ... insider speech. Not useful or helpful if you don't come with enough somehow already related experience. – Claudio Mar 03 '23 at 21:42

3 Answers3

2

As described in the duplicate at Bash, execute command but continue with interactive session, --rcfile can be used to pass bash code to run on startup:

import tempfile, subprocess

with tempfile.NamedTemporaryFile(prefix='bash-rcfile') as tf:
  tf.write(b'resize -s 5 32\n')
  tf.flush()
  subprocess.run(['gnome-terminal', '--', 'bash', '--rcfile', tf.name, '-i'])
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 1
    I'd have to spend some time reading the POSIX sh spec to give you a _good_ answer on "why". The short/bad/high-level answer is that bash is required to maintain compatibility with a specification for `/bin/sh` that never anticipated GNU-style long options (`--word`), and it needs to handle those options in such a way as to not violate any black-letter content of the spec while doing so; so they're accepted only in positions where POSIX doesn't say anything contrary. _Exactly what_ POSIX specifies that conflicts would call for further research, and another question/answer pair all its own. – Charles Duffy Mar 03 '23 at 22:19
  • I'm hesitant to describe "marry the Terminal" as a thing that only happens if the interpreter is going to be interactive -- from the terminal's perspective the shell is behaving the same way whether it stays alive and becomes interactive (exiting when the user later requests it) or runs a script and exits; in either of those cases the terminal stays active either until its direct child process finishes (at which time everything still trying to use that terminal would get a SIGHUP) or until there's no process remaining that reads from or writes to the file handles it passed to that child. – Charles Duffy Mar 03 '23 at 23:11
  • BTW, rcfile isn't _strictly_ the only way to give bash instructions to follow while still becoming interactive after; one can do the same thing with `BASH_ENV`, f/e. – Charles Duffy Mar 03 '23 at 23:14
  • (Pardon the restatement -- some of my earlier comment revisions weren't quite accurate; ended up ripping them out to pare them down) – Charles Duffy Mar 03 '23 at 23:15
  • Thank you much for your effort of providing helpful comments and well working code in your answer. It put me on a path of better understanding what is going on. I have used all of the information gathered from your answer and all of the comments to write a more complete answer addressing my main question and many of the other aspects of the question. – Claudio Mar 04 '23 at 10:56
0

In the world of programming where you need your code to have been written exactly according to the maybe hard to understand rules, you can use another approach to achieve your goal relying only on the user interface GUI of the applications instead of their API exposed for usage in programming.

This way you just start a Terminal and then use an automation tool like for example pyautogui to start Python in the already running interactive Terminal with a bash prompt.

It is a very simple to go path and will work the same way for any shell in any system environment, so you don't need to cope with cases where the software has bugs in the programming or command-line API or does not implement expected internal standards focusing only on providing a well functioning user interface.

An apparent advantage of this approach is that you can SEE yourself on the screen what is done on the path to the result.

Try the code below to obtain the required window size and a running Python shell session which if finished will end up with still opened Terminal window and a ready to use shell:

import os
import pyautogui
import time
from subprocess import run
run(["gnome-terminal","--geometry=32x5"])
time.sleep(0.25)
pyautogui.write("python\n")

Sure ... such approach CAN and will fail if exposed to some conditions like fast user multitasking or system based reasons.

Actual it seems that a 0.25 sec. time span (on my machine 0.1 is already enough) between starting a window and sending it keystrokes is sufficient to make the code work as expected. And if not ... just increase this value ... hoping to not face weird circumstances. And avoiding switching to another windows within tenths of a second ;) .

Claudio
  • 7,474
  • 3
  • 18
  • 48
  • "Some conditions"... like the user trying to do anything in a different window at the same time. Multitasking is not exactly rare or unusual. – Charles Duffy Mar 03 '23 at 21:10
0

Let's summarize what was said in the comments and what was provided as answers and along with doing that actually answer your prime question phrased as What is wrong with my expectation that both lines should give an interactive Terminal window?

The problem with your expectations is twofold. The first aspect of your (failing facing the facts) expectation is to expect that in a Terminal started Python session will fall back to a bash prompt if exited. The Terminal can't fall back into the bash session in that case just because when the Terminal starts the Python session bash isn't involved at all and the Terminal communicates directly only with Python. So there is no bash here to fall back to when Python exits.

The second aspect is to expect that a -i option of the bash command short explained in ~ $ man bash and ~ $ info bash as If the -i option is present, the shell is interactive will make the bash started with this option not exiting but staying interactive. In other words the core of experienced confusion is to expect that a word in a software manual mirrors its intuitive and usual spoken language meaning.

In the world of software if running into confusion it may be necessary to look at the very details in the user manual to see what a used word actually means. This is here the case for the -i option of the bash shell and the reason for this is most probably of historical nature and can be short nailed down to the fact that a started bash when given something to do just does the job and exits.

To keep the shell alive when given some preliminary commands it is necessary to use its -i option along with providing the preliminary commands as content of a bash script which file name is to specify using the long --rcfile option followed by the short -i option as the other way around just does not work probably because of the necessity to fulfill some POSIX standards. By the way: you may (haven't tested it yet) set a BASH_ENV environment variable before starting the shell instead of using the --rcfile option.

While what is stated above already fully answers your question let's give here as nice add-ons and for the sake of completeness also the Python code from the answer to your question provided by Charles Duffy and some further explanations given in the comments:

The code below is able to get the bash shell to do some preliminary work and then still stay alive in the Terminal when its job of resizing the terminal window and/or starting a Python session is done.

The code uses the more modern subprocess module instead of an os.system() call which (saying it with words by Charles Duffy) "creates an extra copy of /bin/sh, with all the extra complexities that come from needing an extra layer of escaping on top of your command (instead of being able to just pass the argument vector directly)". The code requires also the usage of the in Python distribution available tempfile module as a work around for creating an actual file bash then can get its preliminary commands from:

import tempfile, subprocess

with tempfile.NamedTemporaryFile(prefix='bash-rcfile') as tf:
  tf.write(b'resize -s 5 32\npython\n')
  tf.flush()
  subprocess.run(['gnome-terminal', '--', 'bash', '--rcfile', tf.name, '-i'])

The code above creates a Terminal window with an in it running interactive Python session and the special coded feature here is that when the Python session is exited, the Terminal window stays alive and open providing a bash shell prompt.

For the sake of completeness here also the explanation how it comes that the one line in your code runs ok without a space and the other does not (provided in the comments to the question by Socowi):

Short answer: Every program decides how to interpret its options. Longer answer: Because every program decides on its own what to do with its options, -i can have an entirely different meaning between two different programs. They don't even have to accept options like -i but could even decide to use things like °:#§i__. Same goes for (optional) spaces. Complete Answer: The actual meaning of python -i and bash -i options can be found using ~ $ man python and ~ $ man bash. – Socowi

By the way: instead of using resize to set the geometry of started Terminal window you can directly use the --geometry parameter supported by gnome-terminal setting at the same time not only the size, but also the position of the created Terminal window:

from tempfile   import NamedTemporaryFile as ntf
from subprocess import run
with ntf() as tf:
  tf.write(b'python\n') # commands for the job to be done by the shell
  tf.flush()
  run([ 'gnome-terminal', '--geometry=32x5+200+200', '--', 
            'bash', '--rcfile', tf.name, '-i'])

NOTICE in the code above that X,Y (i.e. width and height) specified in the --geometry option should be swapped compared to their order in resize -s

FINALLY The question What does the bash option -i actually do when bash is invoked from Python? would need much of further text for accurate explanations. One is sure: the -i alone isn't enough to make bash interactive. The code above provides a usage case for this option which does what was requested in the question. This should be enough as an answer.

Claudio
  • 7,474
  • 3
  • 18
  • 48