-
Notifications
You must be signed in to change notification settings - Fork 2
/
README.txt
307 lines (231 loc) · 10.9 KB
/
README.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# TODO.PY
## A Python script for managing a personal Todo.txt file.
This script can be used to interact with `todo.txt`, a text file which represents the _next actions_ list in a GTD-like productivity system. It has support for _contexts_ (different kinds of tasks) and _projects_ (grouping tasks into arbitrary groups). You can quickly view all the current tasks, certain contexts and projects, or a combination of both. Tasks can be quickly added and removed from the `todo.txt` file from the command line.
Todo.py doesn't do priorities, or deadlines, or anything else. It's a simple example of how Python can read and write plain text files, created as a teaching example. The rest of this `README` file contains a walk through the original version of the source code, published on [Two Stops Down][1] in May of 2009.
[1]: http://www.twostopsdown.com/2009/04/30/using-python-to-manage-todotxt
## History
24/04/2011: Original script and article as they appeared on Two Stops Down.
# Using Python to Manage Todo.txt
As my first experiment with the Python programming language, I made a
script that helps me look at things in my `todo.txt` file.
Even if you don't know Python, this should be pretty easy to follow. I'll step
through it with you!
#!/usr/bin/env python
#Filename: todo.py
# A script to do things with todo.txt
These lines are just housekeeping. We're saying where Python is on the system
and what we're here to do.
import os
import sys
import re
Bringing in a few things to help us do things. `os` helps us get the location
of the todo.txt file, `sys` allows us to read command line arguments, and `re`
lets us do regular expressions.
# Configuration
todo_path = os.path.expanduser('~/todo.txt')
Somewhere for us to set the location of our file. The `os.path.expanduser`
replaces the `~` with our actual home directory.
Now we're onto our **functions**. These are things our program will do, and we
define them here rather than write it out every time we use it.
def LoadTodos():
global todos
todo_file = open(todo_path, 'r')
raw_todos = todo_file.readlines()
todo_file.close()
todos = []
for item in raw_todos:
item = item.strip("\n")
todos.append(item)
Our first function, `LoadTodos()`, does a few things. First of all, it opens
our `todo.txt` in a read-only mode. Next up, it pulls all the text out and
puts all the lines of text into a list (array). Just to finish up, we go
through each item and remove any line breaks - we add them ourselves later on.
def WriteTodos(arguments = ""):
LoadTodos()
todo_file = open(todo_path, 'a')
# Is there a newline aready here for us? If not, add it.
if todos[-1][-1] != "\n":
todo_file.write("\n")
# If we've been given something, add each of them followed by a new line.
if arguments != "":
for item in arguments:
todo_file.write(str(item) + "\n")
todo_file.close()
This function adds things to our `todo.txt`. This time, the file is opened in
append mode. This mode means every time we write to the file, we're just
adding on to the end of it.
Once we have the file, we check to see if we're on a new line already. We
check the very last character of the very last line and see if it's a new line
`\n`.
Finally, we check if we have any data to add, and then write it to the file.
We also add a newline (just a convention of mine).
def ShowResults(results, numbers = 1):
print('')
if len(results) == 0:
print("No results.")
else:
i = 1
for item in results:
if numbers == 1:
print("{0}. {1}".format(i, item))
else:
print(item)
i += 1
print('')
Our last function is used to show our results. This is more complex.
This function takes two arguments. It takes the results we're going to show,
as well as something called 'numbers', which we set to 1 by default. This
variable says if we want to enumerate the results or not.
Firstly we run a sanity check. If we have no results, we have nothing to show.
So we say just that.
If we do have results, we start a counter, and iterate through them. If we're
enumerating, we print them like "1. First item", "2. Second item", etc. If
not, we just print it.
# And on with the show...
# If we've got nothing to do, show what we've got!
if len(sys.argv) == 1:
LoadTodos()
ShowResults(todos)
sys.exit()
If someone just runs the script by itself (i.e., there are no arguments), we
run our `LoadTodos() `function and show the results. We then run sys.exit() to
finish up early.
# What if we want to look up something?
arg = sys.argv[1:]
results = []
Here's some more housekeeping. Notice how earlier, to check if there were no
arguments, we checked to see if `sys.argv` (a list of command line arguments)
had _one_ entry? The command itself is included as a command line argument, so
we have to trim that off somehow. Here, we make a new variable, `arg`, and
give it all the arguments bar the first (`sys.argv[0]`).
We also start an empty list called results, for when we need it.
if arg[0] == "=" or arg[0] == "@":
regex = str(arg[0]) + r"\S*"
projects = set()
LoadTodos()
for item in todos:
matches = re.findall(regex, item)
for match in matches:
match = match.strip("]")
projects.add(match)
ShowResults(sorted(projects), 0)
Now we have our arguments, we can start to parse it. Here we say, if the first
argument we get is an 'equals' sign or an 'at' sign, we load our `todo.txt`
and find all words starting with an equals or an at.
Just to explain my `todo.txt`: Each line is an action item, and optionally a
project (prefixed with an equals sign), and a context (prefixed with an at
sign). For example, if I have something to do with my website that I could
only do at my desk, it would appear like:
Change all the fonts to Crazy ITC. [=website @desk]
I usually keep these tags in square brackets, for purely aesthetic reasons.
Back to the code!
Whenever we find a word beginning with an equals or at sign, we add it to
projects, which is a **set**. A set is like a list, except _its items are
unique_. If you try to add something it already has, nothing happens. That
way, we get a unique-ified list of all our projects or contexts. We then use the
ShowResults() function to show the results in a list, with the optional number
parameter set to 0.
Here's an example output:
> todo =
=photography
=website
=dinner
=cookies
To continue:
elif arg[0][0] == '@' or arg[0][0] == '=':
#Yes, we are looking for something! Let's find it.
search_terms = "|".join(arg)
regex = re.compile(search_terms)
LoadTodos()
for item in todos:
if re.search(regex, item):
results.append(item)
ShowResults(results)
Next, we check to see if the first _character_ of the first argument is an
equals or an at sign. In this case, we assume we're wanting to look at items
that belong to certain projects or contexts.
To do this, we're putting together a regular expression, separating each of
our arguments by a bar. In regular expressions, this means 'or', so it picks
up items that contains any of the arguments. We put these all in our results
list, and then show them.
In practice:
> todo @town @home
1. Take out the trash [@home]
2. Bank heist [@town =mafia]
Onward:
elif arg[0][0] == '+': # Emulate an "AND" operator.
LoadTodos()
old_results = todos[:] # Start with all the items.
for term in arg[1:]: # Iterate through each of the terms
new_results = []
regex = re.compile(term)
for item in old_results:
if re.search(regex, item):
item = item.strip('\n')
new_results.append(item) # Move matching items into a new list
old_results = new_results # ... which goes on to the next round.
ShowResults(new_results)
Now we're getting deep. We want to do the same as before, but we want items
that match _all_ of the arguments. This is tricky to do in a single regular
expression, as the terms could be in any order in the `todo.txt` file.
To get around this we start with a list of all of our `todo.txt` items, and
iterate through each of our arguments, filtering the list down term by term.
Matching items are put into a new variable, which is then filtered by the next
argument, and so on. The remaining results are then shown.
For example:
> todo @school =photography
1. Ask repographics about A2 prints, kinds of paper? [@school =photography =bigprints]
2. Play baseball with the Canon 1500mm f/5.6 [@school =sport =photography]
Let's continue:
elif arg[0] == "done":
search_terms = "|".join(arg[1:])
regex = re.compile(search_terms)
LoadTodos()
for item in todos:
if re.search(regex, item):
results.append(item)
i = 1
numbered_results = {}
for item in results:
numbered_results[i] = item
i += 1
ShowResults(results)
to_del = input("? ")
if to_del != "q":
new_todos = []
for item in todos:
if item != numbered_results[int(to_del)]:
new_todos.append(item)
todo_file = open(todo_path, 'w')
todo_file.write("\n".join(new_todos))
todo_file.close()
Last but not least, one of the hardest. What's the easiest way to delete items
from our todo list? We're already numerating our search results, so with this
solution, we input the number of the item that's completed, and it removes it
from the file. To be specific, it opens our `todo.txt` in a **write** mode.
When we write to the file now, it completely overwrites it with our new list.
I won't go into depth on what's going on here, but essentially we're cloning
our first searching mechanism. We're then showing them to the user, asking for
a number, and then numerating the results for Python using a _dictionary_ (the
same as a two-dimensional array in PHP). This is just so Python can relate the
number it gets to an actual entry.
It then goes through the `todo.txt`, find the item we completed, and removes
it from its own internal copy of the list. We then write it out to the file,
using the join method to convert our array into a text stream.
Here's an example of it in action:
> todo done @home
1. Take out the trash [@home]
? 1
This removes the entry `"take out the trash"` from `todo.txt`.
else:
# Looks like we want to add something instead.
WriteTodos(arg)
Well, all right. One more thing. If we've entered something else that we
haven't identified so far, we use our `WriteTodos()` function to add a new
entry to our file. For example:
> todo "Give up on Ruby [=python]"
… adds the entry to the file.
It's a big job for such a simple task, but it's a good demonstration of some
simple file input-out, as well as integrating regular expressions into Python
functions.
I hope this was an interesting look at some basic code. Take care!