cheatcheat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cheatcheat/__init__.py ADDED
File without changes
cheatcheat/main.py ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ import sys
5
+ import shutil
6
+ import subprocess
7
+ import glob
8
+ import re
9
+
10
+ try:
11
+ import yaml
12
+ except ImportError:
13
+ print("Error: PyYAML is not installed. Please install it using 'pip install PyYAML'.")
14
+ sys.exit(1)
15
+
16
+ def load_config(config_path):
17
+ """Loads configuration from yaml file."""
18
+ if not os.path.exists(config_path):
19
+ print(f"Configuration file not found: {config_path}")
20
+ sys.exit(1)
21
+
22
+ with open(config_path, 'r') as f:
23
+ try:
24
+ return yaml.safe_load(f)
25
+ except yaml.YAMLError as e:
26
+ print(f"Error parsing config file: {e}")
27
+ sys.exit(1)
28
+
29
+ def get_cheatpaths(config):
30
+ """
31
+ Returns a list of cheatpaths from config, ensuring local .cheat dirs are included.
32
+ Returns list of dicts: {'name': str, 'path': str, 'readonly': bool, 'tags': list}
33
+ """
34
+ paths = []
35
+
36
+ # 1. Configured paths
37
+ if 'cheatpaths' in config:
38
+ for cp in config['cheatpaths']:
39
+ expanded_path = os.path.expanduser(cp['path'])
40
+ path_entry = cp.copy()
41
+ path_entry['path'] = expanded_path
42
+ paths.append(path_entry)
43
+
44
+ # 2. Local .cheat directory (highest priority, so maybe prepend? README says "append ... to the cheatpath"
45
+ # but also "will be treated as the most local ... and will override less local".
46
+ # Usually "override" means it is checked *first*.
47
+ # The README says: "The most global cheatpath is listed first in this file; the most local is listed last."
48
+ # "For example, if there is a 'tar' cheatsheet on both global and local paths, you'll be presented with the local one by default."
49
+ # So we should iterate in reverse order to find the "winner", or iterate in priority order (local first) to find the first match.
50
+ # Let's construct a list where the *last* element has the *highest* priority, matching `conf.yml` structure description.
51
+
52
+ local_cheat = os.path.join(os.getcwd(), '.cheat')
53
+ if os.path.isdir(local_cheat):
54
+ paths.append({
55
+ 'name': 'local',
56
+ 'path': local_cheat,
57
+ 'readonly': False,
58
+ 'tags': ['local']
59
+ })
60
+
61
+ return paths
62
+
63
+ def find_cheatsheet(cheatname, paths):
64
+ """
65
+ Finds a cheatsheet by name.
66
+ Searches from most local (last in list) to most global (first in list).
67
+ Returns (path_entry, full_path) or (None, None).
68
+ """
69
+ # Iterate backwards (most local first)
70
+ for path_entry in reversed(paths):
71
+ base_dir = path_entry['path']
72
+ # Check for exact match or match with extension
73
+ # We need to handle subdirectories too, e.g. cheatname = "foo/bar"
74
+
75
+ # Security check: prevent breakout
76
+ target_path = os.path.join(base_dir, cheatname)
77
+ if not os.path.abspath(target_path).startswith(os.path.abspath(base_dir)):
78
+ continue
79
+
80
+ # Check file exact
81
+ if os.path.isfile(target_path):
82
+ return path_entry, target_path
83
+
84
+ # Check file with any extension? README says "file is named 'bar' or 'bar.EXTENSION'"
85
+ # glob for target_path.*
86
+ matches = glob.glob(target_path + ".*")
87
+ if matches:
88
+ # Pick the first one?
89
+ return path_entry, matches[0]
90
+
91
+ return None, None
92
+
93
+ def list_cheatsheets(paths, filter_path_name=None):
94
+ """Lists all cheatsheets."""
95
+ sheets = set()
96
+
97
+ # Iterate all paths
98
+ for entry in paths:
99
+ if filter_path_name and entry.get('name') != filter_path_name:
100
+ continue
101
+
102
+ base_dir = entry['path']
103
+ if not os.path.isdir(base_dir):
104
+ continue
105
+
106
+ # Walk directory
107
+ for root, dirs, files in os.walk(base_dir):
108
+ for file in files:
109
+ if file.startswith('.'): continue # ignore hidden files
110
+
111
+ # Rel path from base_dir
112
+ abs_path = os.path.join(root, file)
113
+ rel_path = os.path.relpath(abs_path, base_dir)
114
+
115
+ # Remove extension for display?
116
+ # README says: named "tar" or "tar.EXTENSION".
117
+ # If we have tar.md, we display tar.
118
+ base, ext = os.path.splitext(rel_path)
119
+ # If there are multiple extensions or weird dots, this might be tricky.
120
+ # But simple logic: if it's a file, it's a cheat.
121
+
122
+ # Check if it matches the pattern
123
+ sheets.add(base)
124
+
125
+ # Sort and print
126
+ for sheet in sorted(sheets):
127
+ print(sheet)
128
+
129
+ def search_cheatsheets(term, paths):
130
+ """Searches for term in all cheatsheets."""
131
+ found = False
132
+ # Walk all paths
133
+ for entry in paths:
134
+ base_dir = entry['path']
135
+ if not os.path.isdir(base_dir):
136
+ continue
137
+
138
+ for root, dirs, files in os.walk(base_dir):
139
+ for file in files:
140
+ if file.startswith('.'): continue
141
+ abs_path = os.path.join(root, file)
142
+
143
+ try:
144
+ with open(abs_path, 'r', errors='ignore') as f:
145
+ lines = f.readlines()
146
+ for i, line in enumerate(lines):
147
+ if term.lower() in line.lower():
148
+ # Calculate sheet name
149
+ rel_path = os.path.relpath(abs_path, base_dir)
150
+ sheet_name, _ = os.path.splitext(rel_path)
151
+ print(f"{sheet_name}:{i+1}: {line.strip()}")
152
+ found = True
153
+ except Exception as e:
154
+ # Ignore read errors
155
+ pass
156
+ return found
157
+
158
+ def edit_cheatsheet(cheatname, paths, config):
159
+ """Edits a cheatsheet. Handles copy-on-write."""
160
+ # check if it exists
161
+ path_entry, full_path = find_cheatsheet(cheatname, paths)
162
+
163
+ editor = config.get('editor', os.environ.get('EDITOR', 'vi'))
164
+
165
+ if path_entry:
166
+ # Exists
167
+ if not path_entry.get('readonly', False):
168
+ # Writable, just open
169
+ subprocess.call(f"{editor} '{full_path}'", shell=True)
170
+ else:
171
+ # Read-only, need to copy to first writable path
172
+ print(f"Path '{path_entry['name']}' is read-only. Copying to personal path...")
173
+
174
+ # Find last writable path (most local)
175
+ target_entry = None
176
+ for entry in reversed(paths): # Iterate in reverse priority order (Local -> Global)
177
+ if not entry.get('readonly', False):
178
+ target_entry = entry
179
+ break
180
+
181
+ if not target_entry:
182
+ print("Error: No writable cheatpath found.")
183
+ sys.exit(1)
184
+
185
+ # Construct new path
186
+ # We assume cheatsheet name might have subdirs
187
+ new_full_path = os.path.join(target_entry['path'], cheatname)
188
+
189
+ # If extension was present in original, try to preserve it?
190
+ # get extension from full_path
191
+ _, ext = os.path.splitext(full_path)
192
+ if ext and not new_full_path.endswith(ext):
193
+ new_full_path += ext
194
+
195
+ # Ensure dir exists
196
+ os.makedirs(os.path.dirname(new_full_path), exist_ok=True)
197
+
198
+ # Copy
199
+ shutil.copy2(full_path, new_full_path)
200
+ print(f"Copied to {new_full_path}")
201
+
202
+ # Open
203
+ subprocess.call(f"{editor} '{new_full_path}'", shell=True)
204
+
205
+ else:
206
+ # Does not exist. Create new.
207
+ # Find last writable path (most local)
208
+ target_entry = None
209
+ for entry in reversed(paths):
210
+ if not entry.get('readonly', False):
211
+ target_entry = entry
212
+ break
213
+
214
+ if not target_entry:
215
+ print("Error: No writable cheatpath found.")
216
+ sys.exit(1)
217
+
218
+ new_full_path = os.path.join(target_entry['path'], cheatname)
219
+ # Verify if we should add extension? For now default to no extension or just as is.
220
+
221
+ os.makedirs(os.path.dirname(new_full_path), exist_ok=True)
222
+ # Open editor (editor will create file usually, or we touch it)
223
+ # subprocess.call will open it.
224
+ subprocess.call(f"{editor} '{new_full_path}'", shell=True)
225
+
226
+
227
+ def view_cheatsheet(cheatname, paths, config):
228
+ """View a cheatsheet."""
229
+ path_entry, full_path = find_cheatsheet(cheatname, paths)
230
+
231
+ if not path_entry:
232
+ print(f"No cheatsheet found for '{cheatname}'.")
233
+ # Optional: Print "Did you mean...?"
234
+ sys.exit(1)
235
+
236
+ viewer = config.get('viewer', os.environ.get('PAGER', 'less'))
237
+
238
+ # Run viewer
239
+ try:
240
+ subprocess.call(f"{viewer} '{full_path}'", shell=True)
241
+ except Exception as e:
242
+ print(f"Error opening viewer: {e}")
243
+ # Fallback to cat
244
+ with open(full_path, 'r') as f:
245
+ print(f.read())
246
+
247
+ def main():
248
+ parser = argparse.ArgumentParser(description="Create and view interactive cheatsheets.")
249
+ parser.add_argument("cheatname", nargs="?", help="The name of the cheatsheet to view/edit.")
250
+ parser.add_argument("-e", "--edit", action="store_true", help="Edit a cheatsheet.")
251
+ parser.add_argument("-l", "--list", action="store_true", help="List all available cheatsheets.")
252
+ parser.add_argument("-p", "--path", help="Filter by cheatpath name (used with -l).")
253
+ parser.add_argument("-s", "--search", help="Search for a term among cheatsheets.")
254
+ parser.add_argument("-d", "--directories", action="store_true", help="List configured cheatpaths.")
255
+ parser.add_argument("--conf", default=os.path.expanduser("~/.config/cheat/conf.yml"), help="Path to config file.")
256
+
257
+ # We might need to handle the case where conf.yml is in current dir or specific location?
258
+ # User env: /Users/cche/git/ch/conf.yml. I will default to look there for this task if not specified?
259
+ # Or just rely on the argument.
260
+ # For this specific task, the User provided @[conf.yml], so it's likely expected to work with that or default user config.
261
+ # I'll default to looking in local dir first for dev purposes if not found in standard locations.
262
+
263
+ args = parser.parse_args()
264
+
265
+ # Config resolution
266
+ config_path = args.conf
267
+ # Fallback to local 'conf.yml' if default doesn't exist but local does (for dev context)
268
+ if config_path == os.path.expanduser("~/.config/cheat/conf.yml") and not os.path.exists(config_path):
269
+ if os.path.exists("conf.yml"):
270
+ config_path = "conf.yml"
271
+
272
+ config = load_config(config_path)
273
+ paths = get_cheatpaths(config)
274
+
275
+ if args.directories:
276
+ for p in paths:
277
+ print(f"{p['name']}: {p['path']} (readonly: {p.get('readonly', False)})")
278
+ return
279
+
280
+ if args.list:
281
+ list_cheatsheets(paths, args.path)
282
+ return
283
+
284
+ if args.search:
285
+ search_cheatsheets(args.search, paths)
286
+ return
287
+
288
+ if args.edit:
289
+ if not args.cheatname:
290
+ print("Error: Please specify a cheatsheet to edit.")
291
+ sys.exit(1)
292
+ edit_cheatsheet(args.cheatname, paths, config)
293
+ return
294
+
295
+ if args.cheatname:
296
+ view_cheatsheet(args.cheatname, paths, config)
297
+ return
298
+
299
+ # If no args, print help
300
+ parser.print_help()
301
+
302
+ if __name__ == "__main__":
303
+ main()
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: cheatcheat
3
+ Version: 0.1.0
4
+ Summary: Create and view interactive cheatsheets on the command-line
5
+ Author-email: ChongChong He <chongchonghe@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/chongchonghe/cheatcheat
8
+ Project-URL: Bug Tracker, https://github.com/chongchonghe/cheatcheat/issues
9
+ Keywords: cheat,cheatsheet,cli
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Utilities
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: PyYAML
18
+
19
+
20
+ cheat
21
+ =====
22
+
23
+ `cheat` allows you to create and view interactive cheatsheets on the
24
+ command-line. It was designed to help remind \*nix system administrators of
25
+ options for commands that they use frequently, but not frequently enough to
26
+ remember.
27
+
28
+ Example
29
+ -------
30
+ The next time you're forced to disarm a nuclear weapon without consulting
31
+ Google, you may run:
32
+
33
+ ```sh
34
+ cheat tar
35
+ ```
36
+
37
+ You will be presented with a cheatsheet resembling the following:
38
+
39
+ ```sh
40
+ # To extract an uncompressed archive:
41
+ tar -xvf '/path/to/foo.tar'
42
+
43
+ # To extract a .gz archive:
44
+ tar -xzvf '/path/to/foo.tgz'
45
+
46
+ # To create a .gz archive:
47
+ tar -czvf '/path/to/foo.tgz' '/path/to/foo/'
48
+
49
+ # To extract a .bz2 archive:
50
+ tar -xjvf '/path/to/foo.tgz'
51
+
52
+ # To create a .bz2 archive:
53
+ tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
54
+ ```
55
+
56
+ Usage
57
+ -----
58
+ To view a cheatsheet:
59
+
60
+ ```sh
61
+ cheat tar # a "top-level" cheatsheet
62
+ cheat foo/bar # a "nested" cheatsheet
63
+ ```
64
+
65
+ To edit a cheatsheet:
66
+
67
+ ```sh
68
+ cheat -e tar # opens the "tar" cheatsheet for editing, or creates it if it does not exist
69
+ cheat -e foo/bar # nested cheatsheets are accessed like this
70
+ ```
71
+
72
+ To view the configured cheatpaths:
73
+
74
+ ```sh
75
+ cheat -d
76
+ ```
77
+
78
+ To list all available cheatsheets:
79
+
80
+ ```sh
81
+ cheat -l
82
+ ```
83
+
84
+ To list all cheatsheets on the "personal" path:
85
+
86
+ ```sh
87
+ cheat -l -p personal
88
+ ```
89
+
90
+ To search for the phrase "ssh" among cheatsheets:
91
+
92
+ ```sh
93
+ cheat -s ssh
94
+ ```
95
+
96
+ Cheatsheets
97
+ -----------
98
+ Cheatsheets are plain-text files with optional file extensions. When the extension is excluded, they are named
99
+ according to the command used to view them:
100
+
101
+ ```sh
102
+ cheat tar # file is named "tar" or "tar.EXTENSION"
103
+ cheat foo/bar # file is named "bar" or "bar.EXTENSION", in a "foo" subdirectory
104
+ ```
105
+
106
+ Cheatpaths
107
+ ----------
108
+ Cheatsheets are stored on "cheatpaths", which are directories that contain
109
+ cheatsheets. Cheatpaths are specified in the `conf.yml` file.
110
+
111
+ It can be useful to configure `cheat` against multiple cheatpaths. A common
112
+ pattern is to store cheatsheets from multiple repositories on individual
113
+ cheatpaths:
114
+
115
+ ```yaml
116
+ # conf.yml:
117
+ # ...
118
+ cheatpaths:
119
+ - name: community # a name for the cheatpath
120
+ path: ~/documents/cheat/community # the path's location on the filesystem
121
+ tags: [ community ] # these tags will be applied to all sheets on the path
122
+ readonly: true # if true, `cheat` will not create new cheatsheets here
123
+
124
+ - name: personal
125
+ path: ~/documents/cheat/personal # this is a separate directory and repository than above
126
+ tags: [ personal ]
127
+ readonly: false # new sheets may be written here
128
+ # ...
129
+ ```
130
+
131
+ The `readonly` option instructs `cheat` not to edit (or create) any cheatsheets
132
+ on the path. This is useful to prevent merge-conflicts from arising on upstream
133
+ cheatsheet repositories.
134
+
135
+ If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
136
+ transparently copy that sheet to the first writeable directory in 'cheatpaths' before opening it for
137
+ editing.
138
+
@@ -0,0 +1,7 @@
1
+ cheatcheat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cheatcheat/main.py,sha256=MslE6blgxVnxjGzZ3mHjpeMAoz71tGK270D_xxFHOAM,11583
3
+ cheatcheat-0.1.0.dist-info/METADATA,sha256=g6KNvbz6bggK9jv71hqSHdnVQkDDCyBSNLQL0aGL0_4,3648
4
+ cheatcheat-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
5
+ cheatcheat-0.1.0.dist-info/entry_points.txt,sha256=4LuOywarRBwVxCbpkLTLzkZEb-8fBIvMSlNTsLXbuIc,47
6
+ cheatcheat-0.1.0.dist-info/top_level.txt,sha256=i6HZwer-VZ2knnrjEZFukKETIudfxQtk8dK1i4_rhw4,11
7
+ cheatcheat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cheat = cheatcheat.main:main
@@ -0,0 +1 @@
1
+ cheatcheat