11
\$\begingroup\$

Motivation

I'd like to know how integrate the use of text editors (e.g. emacs, vim, etc.) into command-line Python programs.

Features

I'd like to be able to do the following:

  • Call a text editor from within a program in order to edit a file or buffer

  • Regain control after the text editor exits

  • Use temporary files, i.e. don't leave anything lying around after the program exits

  • Prepopulate the file or buffer with data

  • Get the edited text back for use in the program

Implementation

I wrote a small module named callvim.py (betraying my preferred choice of text editor) which creates a temporary file, populates it with some text, opens it with a text editor (defaults to vim), and prints the modified text:

#!/usr/bin/env python
# -*- encoding: ascii -*-
"""callvim.py

Demonstrates calling a text-editor (e.g. Vim) from within a Python script,
including passing input to the editor and reading output from the editor.
"""

import tempfile
import os
from subprocess import call

# Get the text editor from the shell, otherwise default to Vim
EDITOR = os.environ.get('EDITOR','vim')

# Set initial input with which to populate the buffer
initial_message = "Hello world!"

# Open a temporary file to communicate through (`tempfile` should avoid any filename conflicts)
#
# NOTE: Don't autodelete the file on close!
#       We want to reopen the file incase the editor uses a swap-file.
#
with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False) as tf:

    # Write the initial content to the file I/O buffer
    tf.write(initial_message)

    # Flush the I/O buffer to make sure the data is written to the file
    tf.flush()

    # Open the file with the text editor
    call([EDITOR, tf.name])

# Reopen the file to read the edited data
with open(tf.name, 'r') as tf:

    # Read the file data into a variable
    edited_message = tf.read()

    # Output the data
    print(edited_message)
\$\endgroup\$
1

2 Answers 2

4
\$\begingroup\$

click is a great library for command line processing and it has some utilities, click.edit() is portable and uses the EDITOR environment variable. I typed the line, stuff, into the editor. Notice it is returned as a string. Nice.

So, rather then write any code, you could consider replacing what your working on with these two lines:

(venv) /tmp/editor $ export EDITOR='=mvim -f'
(venv) /tmp/editor $ python
>>> import click
>>> click.edit()
'stuff\n'

Check out the docs https://click.palletsprojects.com/en/7.x/utils/#launching-editors My entire experience:

/tmp $ mkdir editor
/tmp $ cd editor
/tmp/editor $ python3 -m venv venv
/tmp/editor $ source venv/bin/activate
(venv) /tmp/editor $ pip install click
Collecting click
  Using cached https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl
Installing collected packages: click
Successfully installed click-7.0
You are using pip version 19.0.3, however version 19.3.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(venv) /tmp/editor $ export EDITOR='=mvim -f'
(venv) /tmp/editor $ python
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 16:52:21)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import click
>>> click.edit()
'stuff\n'
>>>

I would check out the source code for click.edit() if you want to write your own.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Commented Dec 2, 2019 at 13:29
3
\$\begingroup\$

If I read it correctly, your edited_message would also contain your initial_message, is this expected?

I think this could be fixed by wrapping your input message this way in the file :

<Input message>

Input above this line will be ignored
###########################

<end of file>

You could then do :

edited_message = tf.read()
start_input_index = edited_message.find("###########################")

# Use the +2 to account for the \n
edited_message = edited_message[start_input_index + 2 :]

Maybe this exact code wouldn't work, I didn't test it, but the main idea is there, it would make more sense to your code.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Why should it contain the initial message? It is up to the user to keep it or remove it. It won't stay for sure. \$\endgroup\$ Commented Nov 30, 2019 at 18:03
  • \$\begingroup\$ @409_Conflict I'm not sure I get your point, are you saying you think it shouldn't be automatically removed because the user could remove it themself or that it should be removed because the user would remove it anyways? \$\endgroup\$
    – IEatBagels
    Commented Dec 2, 2019 at 15:05
  • \$\begingroup\$ I’m just saying that, since the user can edit the file to its heart content, you can't make any assumption about what the edited_message string will contain. So your find may return -1 making you skip the first character of valid input. If you want to support the "ignored above that mark" feature, you may have better results using edited_message.split("###########################\n", 1)[-1]. \$\endgroup\$ Commented Dec 2, 2019 at 16:32
  • \$\begingroup\$ Thanks for your submission! However, as @409_Conflict said in their comment, I wasn't really looking to parse the edited text - just retrieve it. \$\endgroup\$
    – igal
    Commented Dec 4, 2019 at 3:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.