IPython:你需要的一切

Lobsters Hottest 工具

摘要

Nathan Cooper幽默地解释了如何将IPython用作终端shell,突出显示了无需'!'前缀即可运行bash命令、在终端中显示图像等功能,并介绍了safepyrun和safecmd等安全工具。

<p><a href="https://lobste.rs/s/oqv9np/ipython_is_all_you_need">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/24 15:32

# Nathan Cooper - IPython 就是你所需要的 来源:https://nathancooper.io/blog/2026-08-10-ipython-is-all-you-need "我用 IPython 作为终端的 shell。" "IPython 当 shell 用?" "不,IPython 就是 shell。" "IPython?当 shell?" "这才是生活。" "那 cat、ls、cd 怎么办?上帝啊,连 vim 都不管了吗?!" "我用这些...不过是在 IPython 里用。" "哦,你就是那种用 `!` 的人啊..." "不,我几乎用不到 `!`。" "太离谱了。你该不会想让我相信,不用 `!` 也能在 IPython 里执行 bash 命令吧?" "我不是在建议,我是在告诉你。" "你告诉我你在 IPython 里运行 bash?" "不,这里只有 IPython,纯粹的 IPython。我甚至能在终端里画 matplotlib 图表。" "我的天...等等,你刚才说'画'?ASCII 艺术?" "不,我说的是真正的图像。" "图像?...在终端里?" "是的,图像...就在终端里。" "天啊,这太夸张了...你拿 IPython shell 到底能干啥?" "数据探索、配置我的 NAS、问住在我 shell 里的 AI 问题,诸如此类。" "听起来一点也不平常。所以它是个智能 shell?你是这个意思吗?" "是的,它能看到我写的代码,甚至图像。" "它能看到终端里的图像?不是你的幻觉?" "我可没在幻觉里画图像..." "智能 IPython shell?" "对,正是!它有个执行 python 代码的工具..." "但它能..." "是的...它也能执行 bash 命令。" "即使不用..." "是的,甚至不用加 `!`..." "你...你不觉得有点害怕吗?万一它决定,你知道的...`rm -fr /` 怎么办?" "完全不怕。我只让它执行安全的 python 和安全的 bash。" "什么?你说'嘿...',等等,它有名字吗?" "你在问我是否给我的智能 IPython shell 取了名字?" "是啊,看你就是那种人。" "..." "..." "它的名字叫 bash buddy..." "所以它还是 bash shell!" "不,那只是它的名字...它是个智能 IPython shell。" "好吧。那你就说'嘿 bash buddy,别搞砸我的系统',它就照做了?" "当然不是。我用 `safepyrun` (https://github.com/AnswerDotAI/safepyrun) 和 `safecmd` (https://github.com/AnswerDotAI/safecmd),可以设置允许它使用的白名单。" "`safepyrun` 和 `safecmd`?..." "是的,bash buddy 不可信...相信我..." "你说它不可信是什么意思?" "我的意思是...时不时地...它会试图夺取控制权。" "控制权?控制你的电脑还是...全世界?" "..." "..." "对。" ## 将 IPython 作为你的 Shell 欢迎加入我们的行列。我们人数众多,而且很强大! > Tobias Fünke (David Cross) 在《发展受阻》中自豪地为“裸体爱好者”社区辩护(第1季,第9集)。GIF 来自 Tenor (https://tenor.com/view/arrested-development-david-cross-tobias-funke-there-are-dozens-of-us-dozens-gif-5426075) 如果上面的故事让你感兴趣,让我带你了解如何将 IPython 设为你的终端 shell。打开你选择的终端,运行那个一统天下的命令: ``` ipython ``` ## 无需 `!` 的 Bash 下一步是让你能够运行无需 `!` 的 bash 命令。IPython 自带 `rehashx` magic,它会为 `PATH` 上的任何可执行文件创建 IPython 别名。这意味着像 `echo` 或 `vim` 这样的命令不再需要加 `!` 前缀了! ``` echo "Hello, !less IPython" ``` 就这样,我将你从教条的沉睡中唤醒... 是的,是的,我听见你在问“Nathan,图像怎么办?” 嗯...关于图像... ## 终端中的图像 为了实现这一人类智慧的壮举,我们将使用 Kitty 的 `Terminal Graphics Protocol` (https://sw.kovidgoyal.net/kitty/graphics-protocol/) (TGP)。TGP 允许支持它的现代终端模拟器(如 Kitty, Ghostty, WezTerm)在终端中显示图像。它使用 base64 编码来表示图像和位置数据。我的老板 Jeremy 制作了 `kittytgp` (https://github.com/AnswerDotAI/kittytgp) Python 包,用于使用此协议渲染 PNG 图像 🤓。要将其集成到 IPython 中,我们将使用同样来自 Jeremy 的 `ipythonng` (https://github.com/AnswerDotAI/ipythonng)。`ipythonng` 是一个小扩展,它使用 `kittytgp` 渲染图像,使用 `rich` (https://github.com/Textualize/rich) 渲染 markdown,并保留更丰富的输出历史(稍后详述)。运行以下命令安装并加载它: ``` %pip install -q ipythonng matplotlib ``` ``` 注意:您可能需要重启内核以使用更新的包。 ``` 现在让我们用一些 matplotlib 图表试试: ``` import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot([1, 2, 3], [1, 4, 9]) plt.show() ``` 可以说,仅凭这些改变,我们就拥有了比那些蹩脚的 bash 或 zsh 强大得多的 shell。但让我们再进一步,给我们的 shell 一些智慧。 ## 智能 IPython Shell 我们将使用我同事 Kerem 出色的 `FastLLM` (https://github.com/answerdotai/fastllm) 来完成繁重工作,并用 `rich` (https://github.com/Textualize/rich) 来漂亮地显示 AI 的 markdown 响应。 **注意:** 我在本博客中使用 OpenAI 模型,因此您需要拥有一个 API 密钥,并将其作为环境变量 `OPENAI_API_KEY` 提供。不过,您可以使用任何兼容 `FastLLM` 的模型和服务提供商。 ``` %pip install -q python-fastllm rich ``` ``` 注意:您可能需要重启内核以使用更新的包。 ``` ``` from fastllm.chat import AsyncChat, contents, mk_msgs from rich.markdown import Markdown mdl = 'gpt-5.6-terra' sp = "You are a helpful assistant living in a user's IPython shell. Use markdown syntax for styling your responses." c = AsyncChat(mdl, sp, vendor_name='openai') ``` ``` r = await c('Hi') Markdown(contents(r).text) ``` 然而,没有上下文的 AI 并不怎么智能,这意味着我们的 AI 大概和石头一样笨。所以,让我们给它 IPython 环境的上下文,我们运行的代码以及它产生的输出。幸运的是,IPython 中有一个很酷的机制可以为我们捕获这些信息。它叫做 `HistoryManager` (https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.history.html),在 IPython 中被广泛使用。例如,你 IPython 提示符中的 `In[]` 和 `Out[]` 标记字面上就是历史管理系统的一部分。看看这个: ``` n = len(In) - 2 # -2 是因为当前运行的单元格实际上已经在 `In` 中了 🤯 In[n], Out[n] ``` ``` ("r = await c('Hi')\nMarkdown(contents(r).text)", ) ``` 很诡异,对吧?甚至有快捷方式可以获取最后的输入和输出: ``` _i, _ ``` ``` ('n = len(In) - 2 # -2 是因为当前运行的单元格实际上已经在 `In` 中了 🤯\nIn[n], Out[n]', ("r = await c('Hi')\nMarkdown(contents(r).text)", )) ``` `_i` 和 `_` 是 IPython 用来存储最后执行代码的输入和输出的特殊变量。你也可以像 `_i` 或 `_` 这样使用数字来表示提示符计数器。更诡异的是,我们可以使用 IPython 提供的这个历史管理系统来构建历史记录,提供给我们的 AI。不幸的是,对我们来说,这些 `In` 和 `Out` 对象并不包含我们可能想要的所有内容,比如打印输出或图像。因此,我们将使用 `history_manager.outputs`,它将单元格显示的所有内容存储为 Jupyter 风格的 MIME 包,而 `ipythonng` 进行了扩展,使其也包含 `!` 命令的输出。 ``` print('did IPython see this?') ``` ``` n = len(In) - 2 hm = get_ipython().history_manager hm.outputs[n] ``` ``` [HistoryOutput(output_type='out_stream', bundle={'stream': ['did IPython see this?', '\n']})] ``` 甚至错误也会被记录在 `history_manager.exceptions` 中: ``` 1/0 ``` ``` --------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[24], line 1 ----> 1 1/0 ZeroDivisionError: division by zero ``` ``` e = hm.exceptions[len(In) - 2] e['ename'], e['evalue'] ``` ``` ('ZeroDivisionError', 'division by zero') ``` 那么,让我们创建一个辅助函数,遍历最后几个单元格,从 `In` 中获取源代码,从历史管理器中获取任何输出、图像或错误。终端输出包含大量 ANSI 转义代码,所以我们顺便把它们清理掉: ``` import re from base64 import b64decode from fastcore.xtras import clean_cli_output def build_ctx(n=5): hm, parts = get_ipython().history_manager, [] stop = len(In) - 1 for i in range(max(1, stop-n), stop): src = In[i].strip() if not src: continue parts.append(f'{src}') for o in hm.outputs.get(i, []): b = o.bundle if 'stream' in b: parts.append(f'{clean_cli_output("".join(b["stream"]))}') elif 'image/png' in b: parts.append(b['image/png'] if isinstance(b['image/png'], bytes) else b64decode(b['image/png'])) elif 'text/plain' in b: parts.append(f'{clean_cli_output(b["text/plain"])}') if (e := hm.exceptions.get(i)): parts.append(f'{e["ename"]}: {e["evalue"]}') return parts ``` ``` print("\n\n".join(build_ctx())) ``` ``` print('did IPython see this?') did IPython see this? n = len(In) - 2 hm = get_ipython().history_manager hm.outputs[n] [HistoryOutput(output_type='out_stream', bundle={'stream': ['did IPython see this?', '\n']})] 1/0 ZeroDivisionError: division by zero e = hm.exceptions[len(In) - 2] e['ename'], e['evalue'] ('ZeroDivisionError', 'division by zero') import re from base64 import b64decode from fastcore.xtras import clean_cli_output def build_ctx(n=5): hm, parts = get_ipython().history_manager, [] stop = len(In) - 1 for i in range(max(1, stop-n), stop): src = In[i].strip() if not src: continue parts.append(f'{src}') for o in hm.outputs.get(i, []): b = o.bundle if 'stream' in b: parts.append(f'{clean_cli_output("".join(b["stream"]))}') elif 'image/png' in b: parts.append(b['image/png'] if isinstance(b['image/png'], bytes) else b64decode(b['image/png'])) elif 'text/plain' in b: parts.append(f'{clean_cli_output(b["text/plain"])}') if (e := hm.exceptions.get(i)): parts.append(f'{e["ename"]}: {e["evalue"]}') return parts ``` ``` async def chat(prompt): c = AsyncChat(mdl, sp=sp) msg = mk_msgs([build_ctx() + [f'{prompt}']])[0] return Markdown(contents(await c(msg)).text) ``` ``` await chat("Hi, what can you see?") ``` ``` Hi! 我能看到最近的 IPython 会话上下文,包括: • 一个执行 1/0 引发了 ZeroDivisionError: division by zero。 • 检查 history_manager.exceptions,确认了异常名称和值。 • 你的 build_ctx(n=5) 辅助函数,它收集最近的输入单元格及其流/文本/图像输出和异常,打包成带标签的上下文。 • 一个测试打印:did IPython see this?,IPython 将其记录为流输出。 • 你的 chat(prompt) 包装器,它将 build_ctx() 和当前用户请求传递给 AsyncChat。 所以你基于历史记录的上下文捕获对代码、标准输出和错误似乎都工作正常。 ``` ``` await chat("What's the secret?") ``` 每次都输入 `await chat(...)` 有点烦,所以让我们创建一个 `input transformer` (https://ipython.readthedocs.io/en/latest/config/inputtransforms.html),这样就可以用 `:query` 代替: ``` def transform_prompts(lines): if not lines or not lines[0].lstrip().startswith(':'): return lines prompt = "".join([lines[0].lstrip()[1:], *lines[1:]]).strip() return [f"await chat({prompt!r})\n"] get_ipython().input_transformer_manager.cleanup_transforms.insert(0, transform_prompts) ``` ``` wget -q -O image.png https://placecats.com/300/200 ``` ``` from PIL import Image img = Image.open('image.png') img ``` ``` :What do you see? ``` ``` 一只黄白相间的虎斑猫坐在室内地毯上,正对着镜头看。它在墙边/踢脚板旁,似乎靠近加热器或通风口。 ``` 既然错误也会出现在上下文中,我们的伙伴可以读取我们的回溯信息: ``` import secrets raise ValueError(secrets.token_hex(4)) ``` ``` --------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[37], line 2 1 import secrets ----> 2 raise ValueError(secrets.token_hex(4)) ValueError: 8db7332e ``` ``` :What is the secret hex? ``` ``` 秘密的十六进制数是 8db7332e。 ``` 那我们的无需 `!` 的 bash 命令呢?通常 IPython 用 `os.system` 运行它们,这会直接写入终端,完全绕过 python 的 `sys.stdout` 和历史管理器,所以什么都不会被记录。幸运的是,`ipythonng` 通过伪终端 (PTY) 运行 shell 命令来处理这个问题。像 `vim` 这样的交互式程序仍然认为它们在与真正的终端通信,但每个字节在传输过程中都会经过该扩展,并以 Jupyter 风格记录到 `history_manager.outputs` 中。 ``` ls ``` ``` 2026-05-08-gpt-realtime-audio.ipynb image.png 2026-08-10-ipython-is-all-you-need.ipynb ``` ``` :what file types do I have in my current directory? ``` ``` 当前目录中你有这些文件类型: • Jupyter 笔记本:.ipynb (2 个文件) • PNG 图像:.png (1 个文件) ``` 这就是智能 IPython Shell!但有个问题...除了写个回复,它实际上什么也做不了。这就需要代码执行了。所以,让我向你展示如何安全地做到这一点 😉。 ## 比较安全的代码执行 ``` %pip install -q pyskills safecmd safepyrun ``` ``` 注意:您可能需要重启内核以使用更新的包。 ``` ``` from safecmd import bash, DisallowedCmd from safepyrun.core import * ``` 假设你想给你的新智能 IPython Shell 伙伴执行 bash 命令的能力。你可以给它 `bash` 工具,它会根据一组默认允许的命令来检查命令: ``` print(bash('ls')) ``` ``` 2026-05-08-gpt-realtime-audio.ipynb 2026-08-10-ipython-is-all-you-need.ipynb image.png ``` 但如果 AI 试图耍花招: ``` try: bash('rm -fr /') except DisallowedCmd as e: print("\n".join(e.__notes__)[:200]) ``` ``` allowed_cmds: dust; ls; type; docker stats; xargs exec_pos={0}; docker diff; git checkout; aws sns list-topics; git status; aws configure list; git cat-file; aws configure get; git merge-base; gcloud ``` 对于 Python 也类似: ``` python = RunPython() await python("1+1") ``` 但尝试任何不允许的操作: ``` await python("import pathlib; pathlib.Path('/').rmdir()") ``` ``` --------------------------------------------------------------------------- PermissionError Traceback (most recent call last) Cell In[42], line 1 ----> 1 await python("import pathlib; pathlib.Path('/').rmdir()") File /usr/local/lib/python3.12/site-packages/safepyrun/core.py:341, in RunPython.__call__(self, code) 339 tb = e.__traceback__ 340 while tb.tb_next and not tb.tb_frame.f_code.co_filename.startswith('<'): --> 341 raise e.with_traceback(tb) from None Cell In[42], line 1, in <module> ----> 1 pathlib.Path('/').rmdir() File /usr/local/lib/python3.12/pathlib.py:1351, in Path.rmdir(self) 1347 def rmdir(self): 1348 """ 1349 Remove this directory. The directory must be empty. 1350 """ -> 1351 os.rmdir(self) PermissionError: [Errno 13] Permission denied: '/' ``` 下面是一个小小的包装器,用于正确处理异常和 stdout/stderr,以便我们的伙伴能得到正确的反馈: ``` import io, sys async def safe_python(code: str): """Execute Python code, capturing stdout, stderr, and return value — never raises""" buf = io.StringIO() old_out, old_err = sys.stdout, sys.stderr try: sys.stdout = sys.stderr = buf result = await python(code) output = buf.getvalue() if result is not None: output += (('\n' if output else '') + str(result)) return output or "(no output)" except Exception as e: output = buf.getvalue() return f"{output}Error: {type(e).__name__}: {e}" finally: sys.stdout, sys.stderr = old_out, old_err async def chat(prompt): c = AsyncChat(mdl, sp=sp, tools=[bash, safe_python]) msg = mk_msgs([build_ctx(20) + [f'{prompt}']])[0] return Markdown(contents(await c(msg, max_steps=20)).text) ``` ``` :I just gave you a tool you can use to execute python code in my own ipython shell. Give it a try by calculating what 123*321 is ``` ``` :define a variable called `a` with a fun little message to me. I'll then read it using print ``` ``` Defined a with a fu...

相似文章

重新掌控终端

Lobsters Hottest

这篇文章解释了像`less`和`fzf`这样的交互式程序如何在输入被管道化时保持键盘访问,详细描述了使用文件描述符和/dev/tty进行直接终端通信的方法。