如何编写一个脚本以打开终端窗口并在其中执行命令?

我有三个脚本需要在启动Ubuntu机器时运行,它们启动我在开发环境中使用的服务。
为了做到这一点,我手动打开三个终端并输入命令。
有没有办法创建一个脚本,可以打开三个终端并在每个终端中执行一个命令?(每个命令应该在单独的终端窗口中,这样我就可以看到它们的输出)。

1十年过去了,我又增加了一个新的答案。请告诉我有哪些调整可以改进给软件开发者使用。 - WinEunuuchs2Unix
8个回答

gnome-terminal -- command

或者

xterm -e command

或者

konsole -e command

相当
terminal -e command

让终端在命令退出后保持打开:

在 konsole 中,有一个 --noclose 标志。

在 xterm 中,有一个 -hold 标志。

gnome-terminal 中,进入 编辑 -> 配置文件首选项 -> 标题。点击 命令 选项卡。从标记为 当命令退出时 的下拉菜单中选择 保持终端打开。您应该为此创建一个新的配置文件,并使用以下命令执行:

gnome-terminal --window-with-profile=配置文件名称 -e 命令

2如果我尝试持有终端,会出现"子进程以状态码127正常退出"的提示。 - Darshan Chaudhary
2gnome-terminal 现在不再具有标题选项 :( - törzsmókus
@törzsmókus 在Ubuntu 14.04上使用gnome-terminal 3.6.2是可以的。你使用的是哪个发行版和gnome-terminal的版本? - bhass1
16.04 LTS,gnome-terminal 3.18.3。@bhass1 现在是2017年... - törzsmókus
1@törzsmókus 确实是2017年!LTS版本拥有5年的支持寿命。14.04版本直到2019年4月才结束。https://wiki.ubuntu.com/Releases - bhass1
1只有在command被引用时,gnome-terminal -e command才能正常工作。所以这样是不起作用的:gnome-terminal -e "echo hello world; sleep 3",但这样是可以的:gnome-terminal -e "bash -c 'echo hello world; sleep 3'"。唉。 - bgoodr
5考虑使用以下命令:gnome-terminal -- command - dallonsi
如何找到默认的应用程序变量或包装器,以便命令可以在任何发行版的终端应用程序中运行?还有,如何找到默认的浏览器或文本编辑器变量? - Kangarooo
我收到了这个警告:“选项“-e”已被弃用,并可能在以后的gnome-terminal版本中被移除。”最好使用--代替-e,就像@dallonsi评论中的示例一样。 - Tim
使用gnome-terminal时,我得到的结果是:“子进程以状态0正常退出。”它会保持终端打开,但无法实际使用。它只是卡住了。 - Kvothe
我想确认任何标志,如 --hold 应该在 -e 之前使用。换句话说,-e 后面应该只跟命令。 - SaidbakR

不要硬编码gnome-terminal,konsole等终端程序,而是使用替代系统。执行默认终端模拟器的程序是:
x-terminal-emulator

在我的系统上,每次执行这个命令时,它都会打开一个新的 Konsole 实例。
幸运的是,终端似乎支持使用 -e 选项来执行命令(我已经验证了 konsolegnome-terminal)。命令后面的参数将传递给被调用的命令。Bash 在我的终端中无法保持打开状态,需要额外的脚本来获取一个终端:
#!/bin/sh
"$@"
exec "$SHELL"

如果你将之前的脚本保存为/home/user/hacky并且设置为可执行文件,你可以使用以下命令来运行你的脚本:
x-terminal-emulator -e /home/user/hacky your-script optional arguments here

需要提供完整的路径,并且`/home/user/hacky`必须可执行。
我之前尝试在新的终端窗口中运行脚本的方法可以在修订版#2中找到,那是在我意识到可以向`x-terminal-emulator`传递参数之前。

在这种情况下,这并没有太大帮助,因为提问者想要做的事情对于所有终端来说并不相同。 - nickguletskii
尝试第三次:这个版本应该保持终端开启,并使用可选参数运行程序。 - Lekensteyn
1我使用了Gnome选项,但是一旦我运行我的脚本,主终端就关闭了!!有任何想法为什么? - McLan
3这是有意为之的设计,当“终端”执行完脚本后会退出,因为没有其他事情要做了。你可以通过调用一个可以执行命令的shell(bash)或者添加一行代码,比如read -p "按回车键继续"来“修复”这个问题。 - Lekensteyn
1如何在终端中运行多个命令?例如cd xxx && start.sh。解释器将&&视为命令的第二部分(这是合乎逻辑的),但如果我对其加引号,它会尝试将整个内容作为一个大参数执行。 - Richard
@Richard 我也是,我想运行多个命令,但是失败了。 - Adam Hunyadi
在我的系统上,x-terminal-emulator 打开的是 Terminator,但我的默认终端是 "Preferred Applications" 中的 xfce4-terminal。为什么会这样呢? - Aaron Franke
@AaronFranke 我猜x-terminal-emulator是通过update-alternatives系统配置来进行设置的,而"Preferred Applications"则是在用户配置中进行配置和存储的。 - Lekensteyn
顺便问一下,在打开的终端模拟器中如何一次运行多个命令? - Rudresh Dixit

2020年2月17日更新:这个答案现在可能已经过时了。

考虑点击此链接并使用我提供的另一个答案:打开带有多个选项卡并执行应用程序的终端


@nickguletskii的回答的帮助下,以及我在他的回答下发表的评论的启发,再加上@grabantot对我的评论的点赞,这是我偏爱的方法,特别是当我想让终端保持打开状态,以便我可以手动使用它。
例如用法:将此脚本添加到启动程序中非常有用,这样脚本就会运行,打开一个终端,创建并命名一个终端选项卡,并为您运行一个命令。或者,您可以将此脚本的符号链接添加到桌面上。我使用这种方法是为了能够双击桌面上的一个图标,然后打开一堆终端(其中各个选项卡根据我要在其中进行的工作命名),以及设置编程环境和日常工作等。
这是一个虚构的示例,它打开一个单独的选项卡,将其命名为"test",然后在其中运行简单的命令cd /etc; ls。末尾的$SHELL部分强制shell保持打开状态,以便您可以看到其输出并继续使用它(我在Stack Overflow的其他地方学到了这一点)。
gnome-terminal --tab --title="test" --command="bash -c 'cd /etc; ls; $SHELL'"

这是一个更复杂的例子,它在同一个gnome-terminal中打开了3个单独的标签页。这正是我桌面快捷方式所做的事情,这样我就可以一次打开多个编程窗口。
gnome-terminal --tab --title="tab 1" --command="bash -c 'cd /etc; ls; $SHELL'" --tab --title="tab 2" --command="bash -c 'cd ~; ls; $SHELL'" --tab --title="tab 3" --command="bash -c 'cd ~/temp3; ls; $SHELL'"

这是上述命令的分解:
  • gnome-terminal = 打开一个gnome终端
  • --tab = 为接下来的内容打开一个独立的选项卡
  • --title="tab 1" = 将此选项卡命名为"tab 1"
  • --command="bash -c 'cd /etc; ls; $SHELL'" = 运行bash -c 'cd /etc; ls; $SHELL'命令,这只是我举例编写的命令;以下是它的作用:
  • bash -c 表示这是一个bash 'c'ommand(命令)
  • cd /etc = 切换到"/etc"路径
  • ls = 列出该目录的内容
  • $SHELL = 这个神秘的片段是必需的,以保持shell打开状态,以便您可以使用它。如果您希望打开shell、运行命令,然后关闭,请删除此部分。但是,我希望选项卡保持打开状态,以便我可以进行编程魔法。 :)
  • 然后我们从--tab部分重新开始,生成选项卡2,再次生成选项卡3。根据您的喜好进行自定义。
截图:

enter image description here


1很高兴我能帮到你) 我还有一些脚本,可以直接点击并开始在项目上工作。它们有两个问题:有很多终端窗口(为它们准备了一个独立的屏幕),以及例如服务器崩溃后窗口关闭的情况。这个答案通过 --tab + $SHELL 解决了我的两个问题。不错! - grabantot
如何打开一个标签并将其设置为活动状态?目前,我打开一个标签后还需要点击它才能使其成为活动标签。 - Alfa Bravo
@AlfaBravo,不太确定。你可以考虑编写脚本来模拟实际的键盘按键,就像是由人类操作一样。例如:xdotool key --clearmodifiers Super+d,我在我的show-desktop.desktop文件中使用这个命令,按下Windows键 + D来切换显示桌面,就好像是人类按下了这些键。我相信你可以编写脚本按下正确次数的Ctrl + PgUpCtrl + PgDn来选择你想要的选项卡,因为这就是人类手动切换选项卡的方式。 - Gabriel Staples
gnome-terminal --tab --title="tab 1" 这个例子正是我所寻找的。然而,在 Ubuntu 22.10 上,我遇到了以下警告:`# 选项“--command”已被弃用,并可能在以后的 gnome-terminal 版本中被移除。

使用“-- ”来终止选项,并在其后放置要执行的命令行。

- tim11g
我尝试了这个建议,但是对于创建多个选项卡的情况不起作用。在第一个“--”后,它会执行第一个选项卡的命令,但然后不解析任何后续的“--tab”选项。当使用多个选项卡和命令时,是否需要不同的语法? - tim11g
@tim11g,这就是为什么我在这个答案的开头添加了:“更新于2020年2月17日:这个答案现在可能已经过时。”然后我链接到一个现在有效的更好的答案。你可以在这里搜索我的~/.bash_aliases文件,找到gs_open_default_tabs函数作为示例。让所有这些都正常工作确实有些棘手,但根据我的测试,在Ubuntu 16.04、18.04、20.04和22.04上都可以正常运行。 - Gabriel Staples

简单来说-
#!/bin/bash

/etc/init.d/ccpd status

这对于其他不需要在终端显示任何内容的命令来说已经足够了。但是在这里必须看到状态显示。
因此,它需要在终端窗口中运行。
#!/bin/bash

gnome-terminal -e "/etc/init.d/ccpd status"  --window-with-profile=NAMEOFTHEPROFILE

另一篇帖子打算使用[]作为占位符

这里的"NAMEOFTHEPROFILE"将被替换为"在命令退出时保持终端的配置文件的名称"。

enter image description here

enter image description here


1@cipricus 我相信 [] 只是一个占位符。 - Karthik T
明白了,但我需要让终端不要关闭得那么快。我猜这个问题也涉及到了链接的那个问题。 - user47206
@cipricus 你试过使用配置文件了吗?只需要在我给出的内容后面加上 --window-with-profile=配置文件名称 - Karthik T
关于个人资料那部分,我仍然不太清楚。也许如果你在回答中非常清楚地解释一下,我就能明白了。但是,如果我按照链接答案中的建议操作,终端还是会保持打开状态:"在gnome-terminal中,转到编辑->个人资料首选项->标题和命令选项卡->从标有“当命令退出时”下拉框中选择“保持终端打开”的选项。" - user47206
1@cipricus 我得回家才能给出更好的指示,但是想法是创建一个带有该选项设置的特殊配置文件,并在上面的位置使用特殊配置文件的名称。 - Karthik T
2如果这对你来说足够了,那就可以。个人资料只不过是一组设置而已。你可以仅为脚本使用设置这些设置,而不必在所有终端中使用它。你可以点击“编辑”->“个人资料”来查看你拥有的所有个人资料,并在其中添加一个按照你在链接帖子中所解释的方式进行设置的个人资料。 - Karthik T

虽然晚了近十年,但这是我用Python的答案。

在下面的 .gif 中,我从现有的终端启动了程序,并运行屏幕录制以展示登录时的样子:

dellstart.gif

我为这个答案编写了一个Python程序。有一些额外的功能,虽然OP没有要求,但对我很有益处:
- 在启动时自动运行,以设置在登录后经常使用的GUI应用程序。 - 打开多个gnome-terminal标签。 - 为终端标签分配标题。 - 将窗口移动到首选位置。 - 在单独的标签中打开gedit和最近打开的五个文件。
这个Python程序:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#==============================================================================
#
#       dellstart - Autostart GUI applications on Dell Fileserver
#
#==============================================================================

'''
CALL:

   dellstart

REQUIRES:

   sudo apt install xdotool
    
'''

from __future__ import print_function           # Must be first import
import os
import time

BASHRC_TIME = 2                                 # Seconds to load ~/.bashrc
WINDOW_TIME = .5                                # Secpmds fpr window to appear

commands = [ 'gnome-terminal &',                # Launch terminal in background
             'sleep '+str(BASHRC_TIME),         # Bash command wait a sec
             'move x y',                        # Move windows to x and/or y
#             'move 2100 1000',                  # triple monitor setup
             'xdotool type "cd ~"',             # Change to home directory
             'xdotool key Return',              # Enter Key
             'xdotool type "./ssh-activity"',   # Suspend after 15 minutes
             'xdotool key Return',              # Enter Key
             'title SSH\ Activity',             # Window title (escape spaces)
             'xdotool key Control_L+Shift_L+T', # Open new terminal tab
             'sleep '+str(BASHRC_TIME),         # Bash command wait a sec
             'xdotool type "cd ~/askubuntu"',   # Change to working directory
             'xdotool key Return',              # Enter Key
             'title Ask\ Ubuntu',               # Window title (escape spaces)
             'gedit',                           # Last 5 files will open up
             'move x y',                        # Move windows to x and/or y
#             'move 3849 2266',                  # triple monitor setup
           ]

""" NOTE: To discover window coordinates, arrange on desktop and type:

        wmctrl -lG
"""

def process_commands(command_list):

    for command in command_list:

        if command.endswith('&'):
            # Launch in background and get window ID opened
            active_pid, active_win = launch_command(command)
            if active_pid == 0:
                print("ERROR launching", command, \
                "Aborting 'dellstart' script")
                exit()

        elif command.startswith('move'):
            move_window(command, active_win)

        elif command.startswith('title'):
            terminal_title(command)

        elif command.startswith('gedit'):
            gedit()

        else:
            run_and_wait(command)


def launch_command(ext_name):
    ''' Launch external command in background and return PID to parent.
        Use for programs requiring more than .2 seconds to run.
    '''

    all_pids = get_pids(ext_name)       # Snapshot current PID list
    all_wins = get_wins(all_pids)       # Snapshot of windows open
    new_pids = all_pids
    new_wins = all_wins
    sleep_count = 0                     # Counter to prevent infinite loops

    os.popen(ext_name)                  # Run command in background

    while new_pids == all_pids:         # Loop until new PID is assigned
        new_pids = get_pids(ext_name)   # Snapshot current PID list
        if sleep_count > 0:             # Don't sleep first time through loop
            time.sleep(.005)            # sleep 5 milliseconds
        sleep_count += 1
        if sleep_count == 1000:         # 10 second time-out
            print('launch_ext_command() ERROR: max sleep count reached')
            print('External command name:',ext_name)
            return 0

    pid_list = list(set(new_pids) - set(all_pids))
    if not len(pid_list) == 1:
        print('launch_command() ERROR: A new PID could not be found')
        return 0, 0

    time.sleep(WINDOW_TIME)             # Give time for window to appear
    new_wins = get_wins(all_pids)       # Snapshot of windows open
    win_list = list(set(new_wins) - set(all_wins))
    if not len(win_list) == 1:
        #print('launch_command() ERROR: New Window ID could not be found')
        #suppress error message because we aren't using window ID at all
        return int(pid_list[0]), 0

    # Return PID of program we just launched in background
    return int(pid_list[0]), int(win_list[0])


def run_and_wait(ext_name):
    ''' Launch external command and wait for it to end.
        Use for programs requiring less than .2 seconds to run.
    '''

    result = os.popen(ext_name).read().strip()
    #print('run_and_wait() command:', ext_name)
    return result


def get_pids(ext_name):
    ''' Return list of PIDs for program name and arguments
        Whitespace output is compressed to single space
    '''
    all_lines = []
    # Just grep up to first space in command line. It was failing on !
    prog_name = ext_name.split(' ',1)[0]
    all_lines = os.popen("ps aux | grep -v grep | grep " + \
                        "'" + prog_name + "'").read().strip().splitlines
    PID = []
    for l in all_lines():
        l = ' '.join(l.split())         # Compress whitespace into single space
        PID.append(int(l.split(' ', 2)[1]))

    return PID


def get_wins(all_pids):
    ''' Return list of all windows open under PID list
        Currently unncessary because we work on active window '''
    windows = []
    for pid in all_pids:
        all_lines = os.popen('xdotool search --pid ' + str(pid)). \
                             read().strip().splitlines
        for l in all_lines():
            windows.append(int(l))

    return windows


def move_window(line, active_win):
    ''' Move window to x y coorindates on Desktop

        If the letter x or y is passed, that dimension remains unchanged eg:

            xdotool getactivewindow windowmove 100 100    # Moves to 100,100
            xdotool getactivewindow windowmove x 100      # Moves to x,100
            xdotool getactivewindow windowmove 100 y      # Moves to 100,y

    '''
    line = ' '.join(line.split())       # Compress whitespace to single space
    x = line.split(' ')[-2]
    y = line.split(' ')[-1]

    # We don't need to pass window ID as last active window defaults
    all_lines = os.popen('xdotool getactivewindow windowmove ' + x + ' ' + y). \
                         read().strip().splitlines
    for l in all_lines():
        print(l)


def terminal_title(new_title):
    ''' Rather awkward calling xdotool which chokes on double quotes and bash
        via python which chokes on backslashes.

        Simple format (if it worked) would be:
            command = r'PS1="${PS1/\\u@\\h: \\w/' + title + '}"'

        The bash function copied from is:
            function termtitle() { PS1="${PS1/\\u@\\h: \\w/$@}"; }

        Reference for xdotool keycodes: 
        https://gitlab.com/cunidev/gestures/-/wikis/xdotool-list-of-key-codes
    '''

    title = new_title.split(' ', 1)[1]   # Strip out leading "title" token

    command = 'xdotool type PS1='
    run_and_wait(command)
    run_and_wait('xdotool key quotedbl')
    command = 'xdotool type $'
    run_and_wait(command)
    run_and_wait('xdotool key braceleft')
    command = 'xdotool type PS1/'
    run_and_wait(command)
    run_and_wait('xdotool key backslash')
    run_and_wait('xdotool key backslash')
    command = 'xdotool type u@'
    run_and_wait(command)
    run_and_wait('xdotool key backslash')
    run_and_wait('xdotool key backslash')
    command = 'xdotool type "h: "'
    run_and_wait(command)
    run_and_wait('xdotool key backslash')
    run_and_wait('xdotool key backslash')
    command = 'xdotool type "w/"'
    run_and_wait(command)
    command = 'xdotool type "' + title + '"'
    run_and_wait(command)
    run_and_wait('xdotool key braceright')
    run_and_wait('xdotool key quotedbl')
    run_and_wait('xdotool key Return')


def gedit():

    last_modified_files = gedit_recent_files()
    command = 'gedit '
    for f in last_modified_files:
        command=command+'"'
        command=command+f
        command=command+'" '
    # Open gedit with last five modfied files
    command=command+' &'
    active_pid, active_win = launch_command(command)
    if active_pid == 0:
        print("ERROR launching", command, \
        "Aborting 'dellstart' script")
        exit()


def gedit_recent_files():
    ''' Get list of gedit 5 most recent files:
    
grep --no-group-separator -B5 'group>gedit' ~/.local/share/recently-used.xbel | sed -n 1~6p | sed 's#  <bookmark href="file:///#/#g' | sed 's/"//g'

/home/rick/python/mmm added=2020-05-02T15:34:55Z modified=2020-11-19T00:43:45Z visited=2020-05-02T15:34:56Z>
/home/rick/python/mserve added=2020-07-26T16:36:09Z modified=2020-11-28T01:57:19Z visited=2020-07-26T16:36:09Z>

    '''
    command = "grep --no-group-separator -B5 'group>gedit' " + \
              "~/.local/share/recently-used.xbel | " + \
              "sed -n 1~6p | sed 's#  <bookmark href=" + '"' + \
              "file:///#/#g' | " + "sed 's/" + '"' + "//g'"

    recent_files = []
    times = []
    all_lines = os.popen(command).read().strip().splitlines
    uniquifier = 1                  # gedit can give all open files same time
    for l in all_lines():
        fname = l.split(' added=', 1)[0]
        trailing = l.split(' added=', 1)[1]
        modified = trailing.split(' modified=', 1)[1]
        modified = modified.split('Z', 1)[0]
        # TODO: 2038
        d = time.strptime(modified, '%Y-%m-%dT%H:%M:%S')
        epoch = time.mktime(d)
        epoch = int(epoch)
        recent_files.append(fname)

        try:
            times.index(epoch)
            # gedit has given multiple files the same modification time
            epoch += uniquifier
            uniquifier += 1
        except:
            pass                    # Not a duplicate time
        times.append(epoch)

    N=5
    top_files = []
    if N > len(times):
        # Less than 5 most recent files in list
        N = len(times)
        if N == 0:
            # No most recent files in list
            return top_files            # return empty list

    # Store list in tmp to retrieve index
    tmp=list(times)
    # Sort list so that largest elements are on the far right
    times.sort()

    #print ('5 most recent from lists and indices')
    for i in range(1, N+1):
        top_files.append(recent_files[tmp.index(times[-i])])

    return top_files


if __name__ == "__main__":

    process_commands(commands)

# end of dellstart

请注意,您可能需要调整系统上的变量 BASHRC_TIME,以使程序运行更快。我的 ~/.bashrc 中有很多函数在运行,而您的可能会运行得更快。
我计划写这个已经很多年了,但直到现在才开始动手。

1我将在20年后写下我的答案,从现在开始。 - emilBeBri
@emilBeBri 噢,那个答案是两年前写的。我还有另一个版本叫做"alienstart",它有两个外部电视加上17英寸的笔记本屏幕。挺酷的。 - WinEunuuchs2Unix

评论Lekensteyn的答案。 我知道这是一个旧帖子,但对于任何发现这个有用的人(就像我刚刚发现的一样) 不要再写一个“hacky脚本”,而是将函数放在你调用的脚本中。
hacky_function()
{
"$@"
exec "$SHELL"
}

调用你的脚本时使用 "x-terminal-emulator -e /path/to/script hacky_function 可选参数"。
别忘了在脚本的末尾加上 "$@"。

更新至我现在使用的版本(2022年)

#!/usr/bin/expect -f
# Get a Bash shell 
spawn -noecho bash
# Wait for a prompt 
expect "$ "
send "sudo /home/user/script.sh arg"
# Hand over control to the user
interact
exit

我无法让这个工作起来。你介意提供更多细节吗? - Hiccup
1@Hiccup Lekensteyn的原始答案需要使用两个独立的脚本,将功能添加到脚本中从而只使用一个脚本。我不再使用这种方法,而是使用expect(已更新答案)。 - Sruli

我可以像这样链接多个选项卡,同时避免使用即将被弃用的--command引起的警告。
gnome-terminal\
    --tab\
        --title="TAB 1" -- bash -c "cd ~; ls; $SHELL"
gnome-terminal\
    --tab\
        --title="TAB 2" -- bash -c "cd ~/Desktop; ls; $SHELL"\

使用screen命令: -d 从现有的screen会话中分离,并重新附加到此处 -m 强制创建一个新的screen会话 -S 创建一个具有指定名称的会话,而不是使用默认名称

1这个回答一点都不清楚,请注意让它更易理解。 - azerafati
@azerafati确实,屏幕不会打开任何终端窗口...这个令人惊叹的软件包甚至没有这样的意图... - m3nda