Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.lang.python > #77407 > unrolled thread
| Started by | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| First post | 2014-09-02 02:11 +1000 |
| Last post | 2014-09-01 19:24 -0700 |
| Articles | 18 — 9 participants |
Back to article view | Back to comp.lang.python
Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 02:11 +1000
Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 02:35 +1000
Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 04:23 +1000
Re: Editing text with an external editor in Python Tim Chase <python.list@tim.thechases.com> - 2014-09-01 15:06 -0500
Re: Editing text with an external editor in Python alister <alister.nospam.ware@ntlworld.com> - 2014-09-02 08:35 +0000
Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 18:45 +1000
Re: Editing text with an external editor in Python alister <alister.nospam.ware@ntlworld.com> - 2014-09-03 08:06 +0000
Re: Editing text with an external editor in Python Terry Reedy <tjreedy@udel.edu> - 2014-09-02 17:14 -0400
Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-03 07:36 +1000
Re: Editing text with an external editor in Python Terry Reedy <tjreedy@udel.edu> - 2014-09-02 21:49 -0400
Re: Editing text with an external editor in Python Zachary Ware <zachary.ware+pylist@gmail.com> - 2014-09-02 22:03 -0500
Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 08:30 +1000
Re: Editing text with an external editor in Python Roy Smith <roy@panix.com> - 2014-09-01 13:06 -0400
Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 04:02 +1000
Re: Editing text with an external editor in Python Cameron Simpson <cs@zip.com.au> - 2014-09-02 08:14 +1000
Re: Editing text with an external editor in Python Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2014-09-02 13:18 +1000
Re: Editing text with an external editor in Python Chris Angelico <rosuav@gmail.com> - 2014-09-02 08:25 +1000
Re: Editing text with an external editor in Python gschemenauer3@gmail.com - 2014-09-01 19:24 -0700
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2014-09-02 02:11 +1000 |
| Subject | Editing text with an external editor in Python |
| Message-ID | <54049ab7$0$29972$c3e8da3$5496439d@news.astraweb.com> |
Python's input() or raw_input() function is good for getting a single line
of text from the user. But what if you want a more substantial chunk of
text from the user? Here's how to call out to an external editor such as
ed, nano, vim, emacs, and even GUI text editors:
import tempfile
def edit(editor, content=''):
f = tempfile.NamedTemporaryFile(mode='w+')
if content:
f.write(content)
f.flush()
command = editor + " " + f.name
status = os.system(command)
f.seek(0, 0)
text = f.read()
f.close()
assert not os.path.exists(f.name)
return (status, text)
Anyone able to test it on Windows for me please?
More here:
https://code.activestate.com/recipes/578926/
--
Steven
[toc] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2014-09-02 02:35 +1000 |
| Message-ID | <mailman.13692.1409589319.18130.python-list@python.org> |
| In reply to | #77407 |
On Tue, Sep 2, 2014 at 2:11 AM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
> Anyone able to test it on Windows for me please?
>
Seems to partially work. I added an 'import os' at the top, and a
simple test call to the function, and it did give me my editor (nano)
and retrieved the text. It did give a warning, though:
----
C:\>Python34\python 123123123.py
cygwin warning:
MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57
Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57
CYGWIN environment variable option "nodosfilewarning" turns off this warning.
Consult the user's guide for more details about POSIX paths:
http://cygwin.com/cygwin-ug-net/using.html#using-pathnames
Your text is:
asdf
Hello, world!
----
Windows doesn't have a nice $EDITOR environment variable to call on,
so I'm not sure what the best way to actually choose an editor is. I
would hope that you can make it externally configurable (I've done
some very weird things with changed editors, like one that connects to
a TCP socket, alerts a server with its arguments, and then SIGSTOPs
itself, and the server sends the file's contents to another
socket-connected client that has a human at the other end, and when it
gets back a response from that client, it rewrites the file and
SIGCONTs the 'editor', which then terminates - so to all intents and
purposes, it's as if that program really did edit the file), but doing
that on Windows may not be easy.
You'll also have to cope with some other possibilities. What happens
if someone tries Notepad? (Don't try this at home. We are experts and
are testing on a closed track. Do not use Notepad unless you, too,
have thirty years of special effects experience.) Turns out it doesn't
like working with a file that another process has open. Nor can SciTE;
it shows an empty file on load, and then is unable to save. I suspect
the comment here is what's biting you:
https://docs.python.org/3.5/library/tempfile.html#tempfile.NamedTemporaryFile
"""Whether the name can be used to open the file a second time, while
the named temporary file is still open, varies across platforms (it
can be so used on Unix; it cannot on Windows NT or later)."""
So it works fairly nicely as long as you're using a Cygwin editor.
Otherwise, not so much. :(
ChrisA
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2014-09-02 04:23 +1000 |
| Message-ID | <5404b987$0$30001$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #77408 |
Chris Angelico wrote:
> On Tue, Sep 2, 2014 at 2:11 AM, Steven D'Aprano
> <steve+comp.lang.python@pearwood.info> wrote:
>> Anyone able to test it on Windows for me please?
>>
>
> Seems to partially work. I added an 'import os' at the top, and a
> simple test call to the function, and it did give me my editor (nano)
> and retrieved the text. It did give a warning, though:
>
> ----
> C:\>Python34\python 123123123.py
> cygwin warning:
> MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57
> Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57
That's arguably a Python bug. Under Cygwin, it should use POSIX paths rather
than Windows paths.
I believe that sys.platform tells you if you are running under Cygwin.
> Windows doesn't have a nice $EDITOR environment variable to call on,
Why not? It's your environment, you can create any environment variable you
like, even under Windows, right?
> so I'm not sure what the best way to actually choose an editor is. I
> would hope that you can make it externally configurable
Read $VISUAL, if it exists, otherwise $EDITOR, if it exists, otherwise fall
back on something hard coded. Or read it from an ini file. Or create an
entry in the register. Whatever. That's up to the application which uses
this function, not the function itself.
Under XP and older the standard DOS editor is EDIT, but that's gone from
Windows 7. Believe it or not, I understand that you can use:
copy con [filename.???]
to do basic line editing, which is terrifying, but I believe it works.
http://stackoverflow.com/a/22000756
And of course, you can install whatever third-party editors you like. There
are Windows ports of nano, vim and emacs.
But fundamentally, the de facto "standard editor" on Windows is Notepad.
> You'll also have to cope with some other possibilities. What happens
> if someone tries Notepad? (Don't try this at home. We are experts and
> are testing on a closed track. Do not use Notepad unless you, too,
> have thirty years of special effects experience.) Turns out it doesn't
> like working with a file that another process has open.
Ah, I feared that would be the case. I'll have to think about a way around
that. It won't be as neat, or as secure, but it should be doable.
--
Steven
[toc] | [prev] | [next] | [standalone]
| From | Tim Chase <python.list@tim.thechases.com> |
|---|---|
| Date | 2014-09-01 15:06 -0500 |
| Message-ID | <mailman.13694.1409602073.18130.python-list@python.org> |
| In reply to | #77411 |
On 2014-09-02 04:23, Steven D'Aprano wrote: > Read $VISUAL, if it exists, otherwise $EDITOR, if it exists, > otherwise fall back on something hard coded. Or read it from an ini > file. Or create an entry in the register. Whatever. That's up to > the application which uses this function, not the function itself. > > Under XP and older the standard DOS editor is EDIT, but that's gone > from Windows 7. Believe it or not, I understand that you can use: > > copy con [filename.???] > > to do basic line editing, which is terrifying, but I believe it > works. And according to [1], the venerable edlin is still available on Win8, even if you don't have edit. Though according to [2], it sounds like MS-EDIT is still available on at least the 32-bit version of Win8 (it doesn't detail whether it comes out of the box on 64-bit Win8). I don't have Win8, so I can't corroborate either application. -tkc [1] http://en.wikipedia.org/wiki/Edlin#History [2] http://en.wikipedia.org/wiki/MS-DOS_Editor
[toc] | [prev] | [next] | [standalone]
| From | alister <alister.nospam.ware@ntlworld.com> |
|---|---|
| Date | 2014-09-02 08:35 +0000 |
| Message-ID | <zjfNv.198388$rQ.139558@fx13.am4> |
| In reply to | #77414 |
On Mon, 01 Sep 2014 15:06:04 -0500, Tim Chase wrote: > On 2014-09-02 04:23, Steven D'Aprano wrote: >> Read $VISUAL, if it exists, otherwise $EDITOR, if it exists, otherwise >> fall back on something hard coded. Or read it from an ini file. Or >> create an entry in the register. Whatever. That's up to the application >> which uses this function, not the function itself. >> >> Under XP and older the standard DOS editor is EDIT, but that's gone >> from Windows 7. Believe it or not, I understand that you can use: >> >> copy con [filename.???] >> >> to do basic line editing, which is terrifying, but I believe it works. > > And according to [1], the venerable edlin is still available on Win8, > even if you don't have edit. Though according to [2], it sounds like > MS-EDIT is still available on at least the 32-bit version of Win8 (it > doesn't detail whether it comes out of the box on 64-bit Win8). > > I don't have Win8, so I can't corroborate either application. > > -tkc > > [1] http://en.wikipedia.org/wiki/Edlin#History [2] > http://en.wikipedia.org/wiki/MS-DOS_Editor if edlin is your only option then it would be better to spend you time writhing your own text editor! Neither edlin or ms-edit are available on my Win 7 installation so I doubt that it has been resurrected for win 8 -- Elbonics, n.: The actions of two people maneuvering for one armrest in a movie theatre. -- "Sniglets", Rich Hall & Friends
[toc] | [prev] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2014-09-02 18:45 +1000 |
| Message-ID | <mailman.13710.1409647561.18130.python-list@python.org> |
| In reply to | #77437 |
On Tue, Sep 2, 2014 at 6:35 PM, alister <alister.nospam.ware@ntlworld.com> wrote: > if edlin is your only option then it would be better to spend you time > writhing your own text editor! Heh! Considering how easy it is to deploy a multi-line edit widget in any GUI toolkit, it shouldn't be too hard to write a GUI text editor. Now, writing a *good* GUI text editor, that's a bit harder. And of course, writing a text/console text editor, that's also less simple. But I put it to you: How do you write your editor up to the point where you can use it to write your editor? We have a bootstrapping problem! ChrisA
[toc] | [prev] | [next] | [standalone]
| From | alister <alister.nospam.ware@ntlworld.com> |
|---|---|
| Date | 2014-09-03 08:06 +0000 |
| Message-ID | <1_zNv.229859$an2.171403@fx09.am4> |
| In reply to | #77438 |
On Tue, 02 Sep 2014 18:45:54 +1000, Chris Angelico wrote: > On Tue, Sep 2, 2014 at 6:35 PM, alister > <alister.nospam.ware@ntlworld.com> wrote: >> if edlin is your only option then it would be better to spend you time >> writhing your own text editor! > > Heh! > > Considering how easy it is to deploy a multi-line edit widget in any GUI > toolkit, it shouldn't be too hard to write a GUI text editor. Now, > writing a *good* GUI text editor, that's a bit harder. And of course, > writing a text/console text editor, that's also less simple. But I put > it to you: How do you write your editor up to the point where you can > use it to write your editor? We have a bootstrapping problem! > > ChrisA fortunately i have a proper operating system now so I don't have have to use edlin. in the unlikely event that a reasonably featured editor is not available I can always use cat :-) -- Nothing is finished until the paperwork is done.
[toc] | [prev] | [next] | [standalone]
| From | Terry Reedy <tjreedy@udel.edu> |
|---|---|
| Date | 2014-09-02 17:14 -0400 |
| Message-ID | <mailman.13719.1409692486.18130.python-list@python.org> |
| In reply to | #77437 |
On 9/2/2014 4:45 AM, Chris Angelico wrote: > On Tue, Sep 2, 2014 at 6:35 PM, alister > <alister.nospam.ware@ntlworld.com> wrote: >> if edlin is your only option then it would be better to spend you time >> writhing your own text editor! > > Heh! > > Considering how easy it is to deploy a multi-line edit widget in any > GUI toolkit, it shouldn't be too hard to write a GUI text editor. Most Python installations have tkinter available. I quickly wrote an absolutely minimal script. import tkinter as tk root = tk.Tk() text = tk.Text() text.pack() root.mainloop() I tested tested the functions and wrote the following. This is a test text entry. Enter and Tab work as expected. The Arrow (Cursor) keys work as expected. CntL-Left and Cntl-Right move a word at time. Home and End move to beginning and end of the line. Cntl-Home and Cntl-Up move to the beginning of the text. Cntl-End and Cntl-Donw move to the end of the text. Shift + cursor movement selects between the begin and end slice positions. PageUp and PageDown are inoperative. Delete and Backspace work as expected. At least on Windows, I can select text and delete, or cut or copy to Clipboard. I can also paste from the clipboard. In otherwords, this is a functional minimal text entry widget. I did not even know about Shift-movement selecting until I tried it. Notepad has this. Thunderbird's text entry does not. I think the above is adequate for most multi-line text entry. In use, a save function would have to be added. A help text with the above info would be good too (Idle needs this). -- Terry Jan Reedy
[toc] | [prev] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2014-09-03 07:36 +1000 |
| Message-ID | <mailman.13720.1409693783.18130.python-list@python.org> |
| In reply to | #77437 |
On Wed, Sep 3, 2014 at 7:14 AM, Terry Reedy <tjreedy@udel.edu> wrote: > import tkinter as tk > root = tk.Tk() > text = tk.Text() > text.pack() > root.mainloop() > > I tested tested the functions and wrote the following. > > This is a test text entry. > Enter and Tab work as expected. > The Arrow (Cursor) keys work as expected. > CntL-Left and Cntl-Right move a word at time. > Home and End move to beginning and end of the line. > Cntl-Home and Cntl-Up move to the beginning of the text. > Cntl-End and Cntl-Donw move to the end of the text. > Shift + cursor movement selects between the begin and end slice positions. > PageUp and PageDown are inoperative. > Delete and Backspace work as expected. > At least on Windows, I can select text and delete, > or cut or copy to Clipboard. > I can also paste from the clipboard. > In otherwords, this is a functional minimal text entry widget. Err, that's not all it takes to run an editor :) File opening and saving would be kinda helpful, for a start... But that's what I meant when I said that a multi-line entry field is an extremely standard widget. (I'm a bit surprised that PgUp/PgDn don't work, but the rest of what you say is perfectly standard.) It's a bit harder to do an editor that doesn't just call on a GUI, and more importantly, "tk.Text().pack()" does not make the world's best editor. But it sure is handy when you want to embed an editor in something else! ChrisA
[toc] | [prev] | [next] | [standalone]
| From | Terry Reedy <tjreedy@udel.edu> |
|---|---|
| Date | 2014-09-02 21:49 -0400 |
| Message-ID | <mailman.13722.1409708996.18130.python-list@python.org> |
| In reply to | #77437 |
On 9/2/2014 5:36 PM, Chris Angelico wrote: > On Wed, Sep 3, 2014 at 7:14 AM, Terry Reedy <tjreedy@udel.edu> wrote: >> import tkinter as tk >> root = tk.Tk() >> text = tk.Text() >> text.pack() >> root.mainloop() >> >> I tested tested the functions and wrote the following. >> >> This is a test text entry. >> Enter and Tab work as expected. >> The Arrow (Cursor) keys work as expected. >> CntL-Left and Cntl-Right move a word at time. >> Home and End move to beginning and end of the line. >> Cntl-Home and Cntl-Up move to the beginning of the text. >> Cntl-End and Cntl-Donw move to the end of the text. >> Shift + cursor movement selects between the begin and end slice positions. >> PageUp and PageDown are inoperative. My mistake. See below. >> Delete and Backspace work as expected. >> At least on Windows, I can select text and delete, >> or cut or copy to Clipboard. >> I can also paste from the clipboard. >> In otherwords, this is a functional minimal text entry widget. 'Minimal' mean minimal, as in the minimal number of lines of code to actually work (4), as a the minimum needed to prove the concept, and a starting point for adding more things. > Err, that's not all it takes to run an editor :) File opening The opening premise of the thread was something like "Input() is good for getting a single line of text input from a user. What about getting multiple lines of text?" Opening a non-empty existing file is not relevant to the purpose of extending input(). I intentionally said 'text entry widget', not 'editor'. For editing a particular existing text, the app might pre-load the widget with that text. > saving would be kinda helpful, for a start... It is really annoying when someone clips (erases) something I said and then says that I should have said what I did say. Here it is again! >> In use, a save function would have to be added. The function does not have to be attached to menu widget. When one imports a patch into a mercurial repository, hg offers the opportunity to enter a multi-line commit message. Just the scenario this thread started with. It opens a text entry window something like tkinter.Text. It is preloaded with something like the following. -----------------------------------X| | | |===================================| |Enter commit message above the line| |or leave blank to abort commit. | |Close window when done. | ------------------------------------- Hg intercepts the window close event to grab the entered text, if any, before it disappears. > But that's what I meant when I said that a multi-line entry field is > an extremely standard widget. (I'm a bit surprised that PgUp/PgDn > don't work, My mistake. They do when I add more lines than fit in the window. -- Terry Jan Reedy
[toc] | [prev] | [next] | [standalone]
| From | Zachary Ware <zachary.ware+pylist@gmail.com> |
|---|---|
| Date | 2014-09-02 22:03 -0500 |
| Message-ID | <mailman.13724.1409713432.18130.python-list@python.org> |
| In reply to | #77437 |
On Tue, Sep 2, 2014 at 3:45 AM, Chris Angelico <rosuav@gmail.com> wrote:
> Considering how easy it is to deploy a multi-line edit widget in any
> GUI toolkit, it shouldn't be too hard to write a GUI text editor.
Coincidentally, I was annoyed enough to write the following program
sometime last week. I was sick of messing with Notepad or Notepad++
as the git editor (and don't feel like learning vim just to write a
commit message), so I took 10 minutes to write this, packaged it up
with py2exe and pointed git at it. Works like a charm for me!
#!C:/Python34/python.exe
"""Simple commit message tool.
Dead simple tkinter GUI for writing commit messages for git without messing
around with Notepad's line ending stupidity or Notepad++ being open or not.
"""
import os
import sys
import tkinter as tk
class Committer:
def __init__(self, root, filename):
self.root = root
self.root.protocol('WM_DELETE_WINDOW', self.destroy)
self.filename = filename
self.text = tk.Text(root)
self.text['width'] = 80
self.text['height'] = 25
self.text.pack()
self.text.focus()
with open(self.filename) as f:
self.text.insert('end', f.read())
self.text.mark_set("insert", "1.0")
def destroy(self):
message = self.text.get('1.0', 'end')
with open(self.filename, 'w') as f:
f.write(message)
self.root.destroy()
def main():
root = tk.Tk()
assert os.path.exists(sys.argv[1]), sys.argv[1]
committer = Committer(root, sys.argv[1])
root.mainloop()
if __name__ == '__main__':
main()
--
Zach
[toc] | [prev] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2014-09-02 08:30 +1000 |
| Message-ID | <mailman.13698.1409610612.18130.python-list@python.org> |
| In reply to | #77411 |
On Tue, Sep 2, 2014 at 4:23 AM, Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote: > Chris Angelico wrote: >> C:\>Python34\python 123123123.py >> cygwin warning: >> MS-DOS style path detected: C:\DOCUME~1\M\LOCALS~1\Temp\tmp94rcwd57 >> Preferred POSIX equivalent is: /DOCUME~1/M/LOCALS~1/Temp/tmp94rcwd57 > > That's arguably a Python bug. Under Cygwin, it should use POSIX paths rather > than Windows paths. Except that I wasn't; I ran Python 3.4 that was installed via the .msi package, and from that Python ran nano that was presumably compiled for Cygwin. >> Windows doesn't have a nice $EDITOR environment variable to call on, > > Why not? It's your environment, you can create any environment variable you > like, even under Windows, right? Sure, but what I mean is, there's a general convention on Unix that setting EDITOR will do that. You don't get to take advantage of expectations that easily on Windows. > But fundamentally, the de facto "standard editor" on Windows is Notepad. Sadly so. Which is why I tried it... >> You'll also have to cope with some other possibilities. What happens >> if someone tries Notepad? (Don't try this at home. We are experts and >> are testing on a closed track. Do not use Notepad unless you, too, >> have thirty years of special effects experience.) Turns out it doesn't >> like working with a file that another process has open. > > Ah, I feared that would be the case. I'll have to think about a way around > that. It won't be as neat, or as secure, but it should be doable. ... and yeah. That's the problem. ChrisA
[toc] | [prev] | [next] | [standalone]
| From | Roy Smith <roy@panix.com> |
|---|---|
| Date | 2014-09-01 13:06 -0400 |
| Message-ID | <roy-FD5EAD.13055101092014@news.panix.com> |
| In reply to | #77407 |
In article <54049ab7$0$29972$c3e8da3$5496439d@news.astraweb.com>, Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote: > import tempfile > > def edit(editor, content=''): > f = tempfile.NamedTemporaryFile(mode='w+') > [...] > command = editor + " " + f.name > status = os.system(command) Hmmm. Didn't we just have a thread about passing external data to shells? $ mkdir '/tmp/;rm -rf;' $ TMPDIR='/tmp/;rm -rf;' python Python 2.7.3 (default, Sep 26 2013, 20:03:06) [GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import tempfile >>> f = tempfile.NamedTemporaryFile() >>> f.name '/tmp/;rm -rf;/tmpW8HFTr' >>>
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2014-09-02 04:02 +1000 |
| Message-ID | <5404b4b5$0$29976$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #77409 |
Roy Smith wrote:
> Hmmm. Didn't we just have a thread about passing external data to
> shells?
>
> $ mkdir '/tmp/;rm -rf;'
> $ TMPDIR='/tmp/;rm -rf;' python
> Python 2.7.3 (default, Sep 26 2013, 20:03:06)
> [GCC 4.6.3] on linux2
> Type "help", "copyright", "credits" or "license" for more information.
>>>> import tempfile
>>>> f = tempfile.NamedTemporaryFile()
>>>> f.name
> '/tmp/;rm -rf;/tmpW8HFTr'
Seems like a lot of trouble to go to to erase your own system. Couldn't you
just run rm -rf / on your own system prior to launching Python?
But seriously, I'm not sure what attack vector you think you have found.
By definition, this is calling out to an external application, which might
do *anything*. It needs to be used in a trusted environment, like any other
tool which calls out to external applications.
[steve@ando ~]$ mkdir foo
[steve@ando ~]$ cd foo
[steve@ando foo]$ git init
Initialized empty Git repository in /home/steve/foo/.git/
[steve@ando foo]$ echo Some content > stuff.txt
[steve@ando foo]$ git add stuff.txt
[steve@ando foo]$ GIT_EDITOR="echo 'you got pwned' #" git commit
you got pwned
Aborting commit due to empty commit message.
I'm not really seeing how this is a security vulnerability. If somebody can
break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment
variables, I've already lost.
As written, the edit() function takes two arguments: the name of an
application to call, and optionally initial contents to be edited.
Obviously the name of the application has to be trusted: either hard code
it, or get it from a trusted source like the VISUAL or EDITOR environment
variables. (It's *your* environment, you can set them to whatever editor
you want. If you want to set them to something hostile, that's your
prerogative.)
If an attacker has already compromised my editor, I've lost.
If I naively run arbitrary code provided by *untrusted* sources (say, I get
the editor from anonymous users over the internet), I've lost. I didn't
think I needed to explicitly say that.
On the other hand, the initial content need not be trusted, since it's just
text. The worst somebody could do is hurt my feelings. (Well, they could in
principle buffer-overflow the editor, or Python, but if you can't trust
Python and your editor to be resistant to that, you shouldn't use them.) If
the initial content could "leak" out and do bad things, that would be a
vulnerability I care about. Having the user destroy their own system by
deliberately misusing the function is not my problem:
# Don't do this. It would be bad.
edit("rm -rf / #")
Have I missed something? I really don't think this is a vulnerability, and I
don't see how using the subprocess module would make it safer.
--
Steven
[toc] | [prev] | [next] | [standalone]
| From | Cameron Simpson <cs@zip.com.au> |
|---|---|
| Date | 2014-09-02 08:14 +1000 |
| Message-ID | <mailman.13696.1409609665.18130.python-list@python.org> |
| In reply to | #77410 |
On 02Sep2014 04:02, Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote:
>Roy Smith wrote:
>> Hmmm. Didn't we just have a thread about passing external data to
>> shells?
>>
>> $ mkdir '/tmp/;rm -rf;'
>> $ TMPDIR='/tmp/;rm -rf;' python
>> Python 2.7.3 (default, Sep 26 2013, 20:03:06)
>> [GCC 4.6.3] on linux2
>> Type "help", "copyright", "credits" or "license" for more information.
>>>>> import tempfile
>>>>> f = tempfile.NamedTemporaryFile()
>>>>> f.name
>> '/tmp/;rm -rf;/tmpW8HFTr'
>
>Seems like a lot of trouble to go to to erase your own system. Couldn't you
>just run rm -rf / on your own system prior to launching Python?
>
>But seriously, I'm not sure what attack vector you think you have found.
>By definition, this is calling out to an external application, which might
>do *anything*. It needs to be used in a trusted environment, like any other
>tool which calls out to external applications.
[...]
>I'm not really seeing how this is a security vulnerability. If somebody can
>break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment
>variables, I've already lost.
[...]
>Have I missed something? I really don't think this is a vulnerability, and I
>don't see how using the subprocess module would make it safer.
It is not just about being hacked.
It is about being robust in the face of unusual setups.
If I were producing this function for general use (even my own personal general
use) it would need to be reliable. That includes things like $TMPDIR having
spaces in it (or other unfortunate punctuation).
On any system where people use GUIs to manipulate files and folders, having
spaces and arbitrary punctuation in pathnames is common. Pointing $TMPDIR at
such a place for a special purpose is not unreasonable.
People keep assuming injection is all about malice and being hacked. It is not.
It is also about robustness and reliability, and possible silent
failure/misfunction.
Cheers,
Cameron Simpson <cs@zip.com.au>
Steph@ensoniq.com says...
| Motorcycle maintenence is an art, isn't it?
By the time you've finished, it's a black art.
- Dave Parry <d.parry@ic.ac.uk>
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2014-09-02 13:18 +1000 |
| Message-ID | <54053724$0$6599$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #77416 |
Cameron Simpson wrote: > It is not just about being hacked. > > It is about being robust in the face of unusual setups. > > If I were producing this function for general use (even my own personal > general use) it would need to be reliable. That includes things like > $TMPDIR having spaces in it (or other unfortunate punctuation). Ah, gotcha. That makes sense. -- Steven
[toc] | [prev] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2014-09-02 08:25 +1000 |
| Message-ID | <mailman.13697.1409610340.18130.python-list@python.org> |
| In reply to | #77410 |
On Tue, Sep 2, 2014 at 4:02 AM, Steven D'Aprano <steve+comp.lang.python@pearwood.info> wrote: > I'm not really seeing how this is a security vulnerability. If somebody can > break into my system and set a hostile GIT_EDITOR, or TMPDIR, environment > variables, I've already lost. Agreed. If I'm calling on your program and setting EDITOR or GIT_EDITOR or whatever to configure how you ask me to edit a file, that's because it's *my* system. The aforementioned setup is actually run as root; the 'editor' quite deliberately does almost nothing, but I know it's safe because I'm the one in control, not because the editor's sanitized. ChrisA
[toc] | [prev] | [next] | [standalone]
| From | gschemenauer3@gmail.com |
|---|---|
| Date | 2014-09-01 19:24 -0700 |
| Message-ID | <845f1fd1-db0d-4559-91ea-b50b34e1e03d@googlegroups.com> |
| In reply to | #77407 |
On Monday, September 1, 2014 11:11:34 AM UTC-5, Steven D'Aprano wrote:
> Python's input() or raw_input() function is good for getting a single line
>
> of text from the user. But what if you want a more substantial chunk of
>
> text from the user? Here's how to call out to an external editor such as
>
> ed, nano, vim, emacs, and even GUI text editors:
>
>
>
> import tempfile
>
>
>
> def edit(editor, content=''):
>
> f = tempfile.NamedTemporaryFile(mode='w+')
>
> if content:
>
> f.write(content)
>
> f.flush()
>
> command = editor + " " + f.name
>
> status = os.system(command)
>
> f.seek(0, 0)
>
> text = f.read()
>
> f.close()
>
> assert not os.path.exists(f.name)
>
> return (status, text)
>
>
>
>
>
> Anyone able to test it on Windows for me please?
>
>
>
>
>
> More here:
>
>
>
> https://code.activestate.com/recipes/578926/
>
>
>
>
>
> --
>
> Steven
here's a full blown text editor (GUI) that gets a "substantial chunk of text" from the user. It's almost finished but is perfectly functional right now. Two files:
main file:
#!/usr/bin/env python
import os
import sys
import glob
import webbrowser
from GtkApp import *
from GPYedit_conf import Preferences
APPLICATION_NAME = 'GPYedit'
class GPYedit(GtkApp_Toplevel):
# For each tab in the notebook, we will store
# our data representing each file (or empty buffer)
# in a list. Each item is a dictionary keeping track of the:
# - Python file object
# - Three components of editing area: scrolled window, textview, and buffer (per tab)
# - Full pathname of the file being edited
# - Text shown in the notebook widget tabs
open_files = [ ]
# Keep track of which buffer we're dealing with.
# Each time the notebook page is switched, this number
# will change (see 'on_nb_page_switched' callback). This value
# is used as the index into the open files list to get at the
# file-specific information and widgets.
current_tab = 0
# User preferences will be accessible through this attribute.
# The Preferences class will be initialized from directives
# found in the gpyedit_settings.ini file.
preferences = Preferences()
def __init__(this):
"""
This is where it all starts. Begin by setting
the window geometry and title and decide whether
to create a new empty file or use the arguments provided.
"""
GtkApp_Toplevel.__init__(this)
this.window.set_title("GPYedit")
(width, height) = GPYedit.preferences.get_window_dimensions()
this.window.set_default_size(width, height)
this.build_GUI()
if len(sys.argv) > 1:
names = sys.argv[1:]
for name in names:
if os.path.exists(name) and os.path.isfile(name):
this.tab_new_from_contents(name)
else:
print 'File "' + name + '" doesn\'t exist.'
else:
this.create_new_file()
def build_GUI(this):
"""
Create the main interface components.
These are
- vbox: Main vertical box for laying out widgets
- menu_bar: self explanatory
- notebook: The tabbed container holding our file buffers
"""
this.vbox = gtk.VBox(False, 0)
this.menu_bar = gtk.MenuBar()
this.notebook = gtk.Notebook()
this.notebook.set_scrollable(True)
this.notebook.connect("switch-page", this.on_nb_page_switch)
this.create_menus()
this.create_toolbar()
this.vbox.pack_start(this.notebook, True, True, 0)
this.window.add(this.vbox)
def create_new_file(this, menu_item = None):
"""
Create a blank buffer with no associated Python file object or name (yet)
NOTE: menu_item is a parameter here because
this method will be used as a signal handler
also for the File menu 'new' item and the prototype
for that handler requires this parameter. It is not used though.
"""
(scrolled_win, textview, buf) = this.editing_area_new()
label = gtk.Label("Untitled")
edit_area = { 'scrolled_window': scrolled_win, 'textview': textview, 'buffer': buf }
# Store everything we know
GPYedit.open_files.append({'file_object': None,
'edit_area': edit_area,
'filename': None,
'label': label})
index = this.notebook.append_page(scrolled_win, label)
this.notebook.show_all()
return GPYedit.open_files[index]
def tab_new_from_contents(this, filename):
"""
Open a new tab and dump the contents of a file
into it.
"""
if this.check_for_used_file_name(filename):
return
(scrolled_win, textview, buf) = this.editing_area_new()
fobj = open(filename, 'r+w')
data = fobj.read()
if data != '': buf.set_text(data)
buf.set_modified(False)
label = gtk.Label(os.path.basename(filename))
edit_area = { 'scrolled_window': scrolled_win, 'textview': textview, 'buffer': buf }
# Store everything we know
GPYedit.open_files.append({'file_object': fobj,
'edit_area': edit_area,
'filename': filename,
'label': label})
index = this.notebook.append_page(scrolled_win, label)
this.notebook.show_all()
return GPYedit.open_files[index]
def open_file(this, menu_item = None):
"""
Open a file. The action performed depends on whether
the current tab with focus has an associated file name.
If it is an empty buffer, then the name the user selects
in the file selector is opened in this tab otherwise
a new one is created to house the new file contents.
"""
# May need to revise a little bit. What happens when there
# is no tab?
error = False
chooser = gtk.FileChooserDialog("Open A File", this.window)
chooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
chooser.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
response = chooser.run()
if response == gtk.RESPONSE_OK:
filename = chooser.get_filename()
if os.path.exists(filename) and os.path.isfile(filename):
if this.check_for_used_file_name(filename):
# Throw error dialog box?
gtk.Widget.destroy(chooser)
return
try:
data = GPYedit.open_files[GPYedit.current_tab]
except IndexError:
# If there are no tabs, we can't grab the file data
# so make sure there's something to work with.
data = this.create_new_file()
if data['filename'] is None and not data['edit_area']['buffer'].get_modified():
obj = open(filename, 'r+w')
contents = obj.read()
data['file_object'] = obj
data['edit_area']['buffer'].set_text(contents) # Insertion..
data['edit_area']['buffer'].set_modified(False) # ..But no user interaction (yet)
data['filename'] = filename
data['label'] = os.path.basename(data['filename'])
this.notebook.set_tab_label_text(data['edit_area']['scrolled_window'], data['label'])
GPYedit.open_files[GPYedit.current_tab] = data
this.set_window_title(filename)
else:
data = this.tab_new_from_contents(filename)
else:
error = gtk.MessageDialog(parent = this.window,
type = gtk.MESSAGE_ERROR,
buttons = gtk.BUTTONS_OK,
message_format = "The file '" + filename + "' doesn't exist!")
gtk.Widget.destroy(chooser)
if error:
error.run()
error.destroy()
def close_file(this, menu_item = None):
"""
Close a file. Determines whether the 'file' to be closed
is just a scratch buffer with some text in it or if it has
a file name (in which case there is an associated Python file object).
If the buffer has been modified since it was either opened or the user
typed some text in, give them a chance to save the data to a file on disk
before removing the tab from the notebook widget.
"""
if this.notebook.get_n_pages() == 0:
return
current_file = GPYedit.open_files[GPYedit.current_tab]
if current_file['edit_area']['buffer'].get_modified() == True:
prompt = gtk.Dialog("Unsaved Changes", this.window)
prompt.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_CLEAR, gtk.RESPONSE_NO,
gtk.STOCK_YES, gtk.RESPONSE_YES)
content_area = prompt.get_content_area()
#name = this.notebook.get_tab_label_text(current_file['edit_area']['scrolled_window'])
if current_file['filename'] is None:
name = 'Untitled'
else:
name = os.path.basename(current_file['filename'])
label = gtk.Label("Save changes to '" + name + "'?")
content_area.pack_start(label, True, True, 0)
prompt.show_all()
response = prompt.run()
prompt.destroy()
if response == gtk.RESPONSE_CANCEL:
return
elif response == gtk.RESPONSE_NO:
if current_file['file_object']:
current_file['file_object'].close()
elif response == gtk.RESPONSE_YES:
(start, end) = current_file['edit_area']['buffer'].get_bounds()
if current_file['filename'] is None:
(action, selected_filename) = this.run_save_as_dialog()
if action == gtk.RESPONSE_OK and selected_filename is not None:
this.save_file(selected_filename,
current_file['edit_area']['buffer'].get_text(start, end))
else:
this.save_file()
else:
if current_file['filename'] is not None:
current_file['file_object'].close()
tab_to_remove = GPYedit.current_tab
this.notebook.remove_page(tab_to_remove)
if this.notebook.get_n_pages() == 0:
this.set_window_title(alt_title = APPLICATION_NAME)
del GPYedit.open_files[tab_to_remove]
def save_file(this, filename = None, data = ''):
"""
Write the contents of a buffer to a file on disk.
"""
if this.notebook.get_n_pages() == 0:
return
current_file = GPYedit.open_files[GPYedit.current_tab]
(start, end) = current_file['edit_area']['buffer'].get_bounds()
text_to_write = current_file['edit_area']['buffer'].get_text(start, end)
if filename is not None:
if len(data) > 0:
obj = open(filename, 'w')
obj.write(data)
obj.close()
else:
# Filename given but no data passed. Dump contents of current buffer
# into a file specified by 'filename' argument.
obj = open(filename, 'w')
obj.write(text_to_write)
obj.close()
else:
# Filename to save to was not provided.
# If the current buffer has no file name, then show a "Save As"
# dialog and prompt them to specify a file name.
if current_file['filename'] is None:
(action, selected_filename) = this.run_save_as_dialog()
if action == gtk.RESPONSE_OK:
current_file['filename'] = selected_filename
current_file['file_object'] = open(selected_filename, 'w+')
current_file['file_object'].write(text_to_write)
current_file['label'].set_text(os.path.basename(selected_filename))
current_file['edit_area']['buffer'].set_modified(False)
GPYedit.open_files[GPYedit.current_tab] = current_file
this.set_window_title(selected_filename)
else:
# Current buffer has associated file name.
# Save to that file.
curr_file_obj = current_file['file_object']
curr_file_obj.truncate(0)
curr_file_obj.seek(0)
curr_file_obj.write(text_to_write)
current_file['edit_area']['buffer'].set_modified(False)
def set_window_title(this, filename = None, alt_title = ''):
"""
Set the window title to a specific string with alt_title
or work with an expected file name. The format for showing
the title information is:
filename (directory path) - application name
"""
if alt_title:
this.window.set_title(alt_title)
elif filename is None:
this.window.set_title("Untitled - " + APPLICATION_NAME) # Default title
else:
(dirpath, fname) = os.path.split(filename)
this.window.set_title(fname + " (" + dirpath + ") - " + APPLICATION_NAME)
def select_all(this):
"""
Select all the text in the editing area.
"""
current_file = GPYedit.open_files[GPYedit.current_tab]
buf = current_file['edit_area']['buffer']
(start, end) = buf.get_bounds()
buf.select_range(start, end)
def copy_to_clipboard(this):
"""
Copy some selected text to the clipboard.
"""
current_file = GPYedit.open_files[GPYedit.current_tab]
clipboard = gtk.clipboard_get()
current_file['edit_area']['buffer'].copy_clipboard(clipboard)
def popup_search_box(this, menu_item = None):
"""
Display the search dialog.
"""
dial = gtk.Dialog("Find and Replace", this.window)
dial.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_CONVERT, gtk.RESPONSE_APPLY,
gtk.STOCK_FIND, gtk.RESPONSE_OK)
dial.set_response_sensitive(gtk.RESPONSE_OK, False)
dial.set_response_sensitive(gtk.RESPONSE_APPLY, False)
table = gtk.Table(4, 2, False)
table.set_row_spacings(8)
table.set_col_spacings(8)
find_label = gtk.Label("Search for:")
find_label.set_alignment(0, 0.5)
replace_label = gtk.Label("Replace with:")
replace_label.set_alignment(0, 0.5)
find_entry = gtk.Entry()
replace_entry = gtk.Entry()
case_sens = gtk.CheckButton("Case sensitive")
replace_all = gtk.CheckButton("Replace all occurences")
table.attach(find_label, 0, 1, 0, 1)
table.attach(find_entry, 1, 2, 0, 1)
table.attach(replace_label, 0, 1, 1, 2)
table.attach(replace_entry, 1, 2, 1, 2)
table.attach(case_sens, 0, 2, 2, 3)
table.attach(replace_all, 0, 2, 3, 4)
content_area = dial.get_content_area()
content_area.pack_start(table)
table.set_border_width(8)
find_entry.connect("insert-text", this.search_buttons_sensitive, dial)
find_entry.connect("backspace", this.search_buttons_insensitive, dial)
dt_id = find_entry.connect("delete-text", this.search_buttons_insensitive_del_text, dial)
find_entry.set_data('del_text_sig_id', dt_id)
widgets = {'find_entry': find_entry,
'replace_entry': replace_entry,
'match_case': case_sens,
'replace_all': replace_all}
dial.connect("response", this.search_dialog_response, widgets)
dial.show_all()
dial.run()
def search_dialog_response(this, dialog, response, widgets):
"""
Process the response returned from the search and replace dialog.
"""
if response == gtk.RESPONSE_OK:
this.document_search(widgets)
elif response == gtk.RESPONSE_CANCEL:
dialog.destroy()
elif response == gtk.RESPONSE_APPLY:
this.document_replace(widgets)
def document_search(this, widgets):
"""
Function not finished
By default, do a forward search on the document in
the tab with focus. To work, this function should
get these widgets:
- find entry text box with the search text
- replace entry with replacement text
- match case checkbox
- text buffer for current file
"""
if widgets['match_case'].get_active():
case_sensitive = True
else:
case_sensitive = False
current_file = GPYedit.open_files[GPYedit.current_tab]
tbuffer = current_file['edit_area']['buffer']
start_iter = tbuffer.get_start_iter()
search_text = widgets['find_entry'].get_text()
(begin, end) = start_iter.forward_search(search_text, gtk.TEXT_SEARCH_TEXT_ONLY)
tbuffer.select_range(begin, end)
def document_replace(this, widgets):
"""
Find a search string and replace it with new text.
By default, only one replacement is done but with
the 'replace all occurences' checkbox selected then
it will perform a global search and replace.
"""
current_file = GPYedit.open_files[GPYedit.current_tab]
tbuffer = current_file['edit_area']['buffer']
start_iter = tbuffer.get_start_iter()
search_text = widgets['find_entry'].get_text()
if widgets['replace_entry'].get_text_length() > 0:
(lower, upper) = tbuffer.get_bounds()
bufdata = tbuffer.get_text(lower, upper)
replace_str = widgets['replace_entry'].get_text()
if widgets['replace_all'].get_active():
# REPLACE ALL
updated_text = bufdata.replace(search_text, replace_str)
else:
# REPLACE ONCE
updated_text = bufdata.replace(search_text, replace_str, 1)
tbuffer.set_text(updated_text)
tbuffer.set_modified(False)
else:
# ERROR: NO REPLACEMENT TEXT GIVEN
pass
def search_buttons_sensitive(this, editable, new_text, new_text_length, pos, search_dialog):
"""
Determine whether the buttons should be sensitive, thereby
allowing the user to search, if there is text in the search box.
"""
if editable.get_text_length() > 0:
return
if new_text_length > 0:
search_dialog.set_response_sensitive(gtk.RESPONSE_OK, True)
search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, True)
def search_buttons_insensitive(this, editable, search_dialog):
"""
Make the search buttons insensitive when there is no
text in the search box.
"""
if editable.get_text_length() == 1:
search_dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, False)
def search_buttons_insensitive_del_text(this, editable, start, end, search_dialog):
"""
Similar to search_buttons_insensitive(), except that this
handler is connected for the 'delete-text' signal. It allows
a user to highlight some text and delete it all at once. In the
case where they select and delete everything in the search box, the
buttons along the bottom of the search dialog should become unusable.
"""
if editable.get_text_length() > 0:
editable.handler_block(editable.get_data('del_text_sig_id'))
editable.delete_text(start, end)
editable.handler_unblock(editable.get_data('del_text_sig_id'))
if editable.get_text_length() == 0:
search_dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
search_dialog.set_response_sensitive(gtk.RESPONSE_APPLY, False)
def check_for_used_file_name(this, name):
"""
Any given file should only be opened in one tab.
This method returns True if a specified file's name
is already in use.
"""
for element in GPYedit.open_files:
values = element.values()
if name in values:
return True
def delete_event(this, widget, event, data = None):
"""
Override method to close all files if a user clicks the 'X'
button to close the text editor without first manually closing
them via the File > Close menu option. Just explicitly cleaning up.
"""
for element in GPYedit.open_files:
file_to_clean_up = element['file_object']
if file_to_clean_up:
file_to_clean_up.close()
def run_save_as_dialog(this):
"""
Display a Save As dialog box and allow the user to specify
a file name to save to.
"""
save_as = gtk.FileChooserDialog("Save As",
this.window,
gtk.FILE_CHOOSER_ACTION_SAVE,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_SAVE, gtk.RESPONSE_OK))
action_id = save_as.run()
save_as.hide()
if action_id == gtk.RESPONSE_OK:
ret_val = (action_id, save_as.get_filename())
else:
ret_val = (action_id, None)
save_as.destroy()
return ret_val
def editing_area_new(this):
"""
Build the set of widgets necessary to allow
for the editing of text. This includes:
- scrolled window: allow viewing area to scroll
- text view: widget to edit text
"""
scrolled_win = gtk.ScrolledWindow()
scrolled_win.set_shadow_type(gtk.SHADOW_ETCHED_IN)
scrolled_win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scrolled_win.set_border_width(3)
textview = gtk.TextView()
textview.set_left_margin(3)
textview.set_right_margin(3)
textview.set_pixels_above_lines(1)
buf = textview.get_buffer()
scrolled_win.add(textview)
textview.modify_base(gtk.STATE_NORMAL,
gtk.gdk.Color(GPYedit.preferences.get_background_color()))
textview.modify_text(gtk.STATE_NORMAL,
gtk.gdk.Color(GPYedit.preferences.get_foreground_color()))
# Set the default font used for editing
textview.modify_font(
pango.FontDescription(GPYedit.preferences.get_font()))
# Return a tuple of the three elements.
# Note that the scrolled window itself holds the
# textview which in turn contains the buffer. But
# this helps avoid having to extract the children of
# the scrolled window every time we need access to either
# the view or the buffer inside it.
return (scrolled_win, textview, buf)
def create_toolbar(this):
"""
Create toolbar and buttons before packing into
the main window.
"""
this.toolbar = gtk.Toolbar()
# Make toolbar widget buttons
this.tb_new = gtk.ToolButton(gtk.STOCK_NEW)
this.tb_open = gtk.ToolButton(gtk.STOCK_OPEN)
this.tb_save = gtk.ToolButton(gtk.STOCK_SAVE)
this.tb_save_as = gtk.ToolButton(gtk.STOCK_SAVE_AS)
# Insert buttons into toolbar
this.toolbar.insert(this.tb_new, 0)
this.toolbar.insert(this.tb_open, 1)
this.toolbar.insert(this.tb_save, 2)
this.toolbar.insert(this.tb_save_as, 3)
this.view_menu_toolbar.connect("toggled", this.toggle_tb_visible)
# Tool bar 'new' button creates a new file. The method signature
# doesn't match the required parameters for this signal though so
# we use a sort of pass-through function to get there.
this.tb_new.connect("clicked", lambda tool_item: this.create_new_file())
this.tb_open.connect("clicked", lambda tool_item: this.open_file())
this.tb_save.connect("clicked", lambda tool_item: this.save_file())
# Pack toolbar into window vbox
this.vbox.pack_start(this.toolbar, False, False, 0)
def toggle_tb_visible(this, widget, data = None):
"""
Callback to control visiblity of the toolbar
"""
if widget.get_active():
this.toolbar.show()
else:
this.toolbar.hide()
def on_nb_page_switch(this, notebook, page, page_num):
"""
Each time the user selects a tab to work with, change
the internal tab indicator so that it can be used to get
the relevant data associated with that tab. This is a callback
and there is no need to call directly. See GTK+ 'switch-page' signal.
"""
GPYedit.current_tab = page_num
file_info = GPYedit.open_files[GPYedit.current_tab]
if file_info['filename'] is not None:
this.set_window_title(file_info['filename'])
else:
this.set_window_title()
def open_by_pattern(this):
"""
Open all files that match a shell-style pattern.
"""
items = glob.glob(os.environ['HOME'] + os.sep + '*.php')
for item in items:
this.tab_new_from_contents(item)
def create_menus(this):
"""
Create the menu bar and associated menu items.
"""
accel_group = gtk.AccelGroup()
# Associate with main window
this.window.add_accel_group(accel_group)
# Create File menu
this.file_menu = gtk.Menu()
this.file_menu.set_accel_group(accel_group)
this.file_menu_item = gtk.MenuItem("File")
this.file_menu_item.set_submenu(this.file_menu)
# Create menu items
this.file_menu_new = gtk.ImageMenuItem(gtk.STOCK_NEW)
this.file_menu_new.add_accelerator("activate", accel_group, ord('n'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.file_menu_open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
this.file_menu_open.add_accelerator("activate", accel_group, ord('o'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.file_menu_open_files_by_pattern = gtk.MenuItem("Open Files By Pattern")
this.file_menu_save = gtk.ImageMenuItem(gtk.STOCK_SAVE)
this.file_menu_save.add_accelerator("activate", accel_group, ord('s'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.file_menu_save_as = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
this.file_menu_save_as.add_accelerator("activate", accel_group, ord('s'), gtk.gdk.SHIFT_MASK | gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.file_menu_close = gtk.ImageMenuItem(gtk.STOCK_CLOSE)
this.file_menu_close.add_accelerator("activate", accel_group, ord('w'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.file_menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
this.file_menu_quit.add_accelerator("activate", accel_group, ord('q'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
# Add them to File menu
this.file_menu.append(this.file_menu_new)
this.file_menu.append(this.file_menu_open)
this.file_menu.append(this.file_menu_open_files_by_pattern)
this.file_menu.append(this.file_menu_save)
this.file_menu.append(this.file_menu_save_as)
this.file_menu.append(this.file_menu_close)
this.file_menu.append(this.file_menu_quit)
# Connect signals
this.file_menu_new.connect("activate", this.create_new_file)
this.file_menu_open.connect("activate", this.open_file)
this.file_menu_open_files_by_pattern.connect("activate", lambda menu_item: this.open_by_pattern())
this.file_menu_save.connect("activate", lambda menu_item: this.save_file())
this.file_menu_close.connect("activate", this.close_file)
this.file_menu_quit.connect("activate", gtk.main_quit)
# Create Edit menu
this.edit_menu = gtk.Menu()
this.edit_menu.set_accel_group(accel_group)
this.edit_menu_item = gtk.MenuItem("Edit")
this.edit_menu_item.set_submenu(this.edit_menu)
# Create menu items
this.edit_menu_cut = gtk.ImageMenuItem(gtk.STOCK_CUT)
this.edit_menu_cut.add_accelerator("activate", accel_group, ord('x'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.edit_menu_copy = gtk.ImageMenuItem(gtk.STOCK_COPY)
this.edit_menu_copy.add_accelerator("activate", accel_group, ord('c'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.edit_menu_paste = gtk.ImageMenuItem(gtk.STOCK_PASTE)
this.edit_menu_paste.add_accelerator("activate", accel_group, ord('v'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.edit_menu_select_all = gtk.MenuItem("Select All")
this.edit_menu_select_all.add_accelerator("activate", accel_group, ord('a'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
this.edit_menu_preferences = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
# Add them to Edit menu
this.edit_menu.append(this.edit_menu_cut)
this.edit_menu.append(this.edit_menu_copy)
this.edit_menu.append(this.edit_menu_paste)
this.edit_menu.append(this.edit_menu_select_all)
this.edit_menu.append(this.edit_menu_preferences)
# Connect signals
this.edit_menu_copy.connect("activate", lambda menu_item: this.copy_to_clipboard())
this.edit_menu_select_all.connect("activate", lambda menu_item: this.select_all())
# Create View menu
this.view_menu = gtk.Menu()
this.view_menu_item = gtk.MenuItem("View")
this.view_menu_item.set_submenu(this.view_menu)
# Create menu items
this.view_menu_toolbar = gtk.CheckMenuItem("Toolbar")
this.view_menu_toolbar.set_active(True)
this.view_menu_file_explorer_pane = gtk.CheckMenuItem("File Browser Pane")
# Add them to View menu
this.view_menu.append(this.view_menu_toolbar)
this.view_menu.append(this.view_menu_file_explorer_pane)
# Create Search menu
this.search_menu = gtk.Menu()
this.search_menu_item = gtk.MenuItem("Search")
this.search_menu_item.set_submenu(this.search_menu)
# Create menu items
this.search_menu_s_and_r = gtk.ImageMenuItem(gtk.STOCK_FIND_AND_REPLACE)
# Add them to Search menu
this.search_menu.append(this.search_menu_s_and_r)
# Connect signals
this.search_menu_s_and_r.connect("activate", this.popup_search_box)
# Create Help menu
this.help_menu = gtk.Menu()
this.help_menu_item = gtk.MenuItem("Help")
this.help_menu_item.set_submenu(this.help_menu)
# Create menu items
this.help_menu_about = gtk.ImageMenuItem(gtk.STOCK_HELP)
# Add them to Help menu
this.help_menu.append(this.help_menu_about)
# Add menus to the menubar
this.menu_bar.append(this.file_menu_item)
this.menu_bar.append(this.edit_menu_item)
this.menu_bar.append(this.view_menu_item)
this.menu_bar.append(this.search_menu_item)
this.menu_bar.append(this.help_menu_item)
# Pack menu bar into main window
this.vbox.pack_start(this.menu_bar, False, False, 0)
########## Main ##########
if __name__ == "__main__": GPYedit().main()
##########################
second file:
import os
from utils import find_file
CONFIG_FILE = "gpyedit_settings.ini"
class Preferences:
"""
This class holds and manages the user's preferences which
will be stored permanently in the gpyedit_settings.ini file.
"""
settings = \
{
"window_width": 779,
"window_height": 419,
"font_face": "monospace",
"background_color": "#FFFFFF",
"foreground_color": "#000000"
}
def __init__(this):
"""
Read the configuration file and set up the
preferences so that they are ready to be accessed
by the main application.
"""
if not os.path.exists(CONFIG_FILE): config_file_location = find_file(CONFIG_FILE, os.environ["HOME"])
else:
config_file_location = os.getcwd() + os.sep + CONFIG_FILE
if config_file_location is None: return # Configuration not found. Use default settings
else:
this.process_options(open(config_file_location, "r").readlines())
def process_options(this, config):
"""
Parse configuration options in the gpyedit_settings.ini file.
"""
for option in config:
data = option.split(":") # Get [option, value]
if len(data) != 2: continue
(opt, val) = (data[0].strip(), data[1].strip())
for setting in this.settings.keys():
if opt == setting:
this.settings[setting] = val
def get_font(this):
"""
Return the font that should be used for editing text.
"""
return Preferences.settings["font_face"]
def get_background_color(this):
"""
Return the background color of the editing area.
"""
return Preferences.settings["background_color"]
def get_foreground_color(this):
"""
Return the foreground color of the editing area.
"""
return Preferences.settings["foreground_color"]
def get_window_dimensions(this):
"""
Retrieve the toplevel window dimensions as a width and height
"""
return (int(this.settings["window_width"]), int(this.settings["window_height"]))
def run_dialog(this):
"""
Create the preferences dialog box accessible through Edit > Preferences in
the menu bar.
"""
---------------------------------------------
maybe that'll help you see how it's done.
[toc] | [prev] | [standalone]
Back to top | Article view | comp.lang.python
csiph-web