6
6
import re
7
7
import os
8
8
9
-
10
- @click .group ()
9
+ FORMATTING_EPILOG = """
10
+ \b
11
+ Format Strings:
12
+ Format strings determine how yamlpal will output values.
13
+ Available keys:
14
+ %{key} : Key of the match (or index if matching an item in a list)
15
+ %{value} : Value of the match
16
+ %{linenr} : Line number where the match occured
17
+ %{file} : Name of the file in which the match occured
18
+ %{literal} : Literal match in the file (original formatting)
19
+ Examples:
20
+ $ yamlpal find "bill-to/address/city" --format "%{file} %{linenr}: %{value}"
21
+ /abs/path/to/examples/examples/sample1.yml 11: Royal Oak
22
+
23
+ $ yamlpal find "bill-to/address/city" --format "%{linenr} %{literal}"
24
+ 11: city : Royal Oak
25
+ """
26
+
27
+
28
+ @click .group (epilog = "Run 'yamlpal <command> --help' for command specific help." )
11
29
@click .version_option (version = yamlpal .__version__ )
12
30
def cli ():
13
- """ Modify yaml files while keeping the original structure and formatting. """
31
+ """ Modify and search yaml files while keeping the original formatting.
32
+ """
14
33
15
34
16
35
def get_files (passed_files ):
@@ -55,9 +74,11 @@ def get_str_content(str_value):
55
74
@cli .command ("insert" )
56
75
@click .argument ('needle' )
57
76
@click .argument ('newcontent' )
58
- @click .option ('-f' , '--file' ,
59
- type = click .Path (exists = True , dir_okay = False , readable = True , resolve_path = True ), multiple = True )
60
- @click .option ('-i' , '--inline' , help = "Edit file inline instead of dumping it to std out" , is_flag = True )
77
+ @click .option ('-f' , '--file' , type = click .Path (exists = True , dir_okay = False , readable = True , resolve_path = True ),
78
+ multiple = True , help = "File to insert new content in. Can by specified multiple times to modify " +
79
+ "multiple files. Files are not modified inline by default. " +
80
+ "You can also provide (additional) file paths via stdin." )
81
+ @click .option ('-i' , '--inline' , help = "Edit file inline instead of dumping it to std out." , is_flag = True )
61
82
def insert (needle , newcontent , file , inline ):
62
83
""" Insert new content into a yaml file. """
63
84
newcontent = get_str_content (newcontent )
@@ -67,6 +88,63 @@ def insert(needle, newcontent, file, inline):
67
88
insert_in_file (needle , newcontent , file , inline )
68
89
69
90
91
+ @cli .command ("find" , epilog = FORMATTING_EPILOG )
92
+ @click .argument ('needle' )
93
+ @click .option ('-f' , '--file' , type = click .Path (exists = True , dir_okay = False , readable = True , resolve_path = True ),
94
+ help = "File to find content in." )
95
+ @click .option ('-F' , '--format' , help = "Format string in which matched content should be returned. " +
96
+ "See the section 'Format Strings' below for details on format strings. " +
97
+ "(default: \" %{key}: %{value}\" )" ,
98
+ default = "%{key}: %{value}" )
99
+ def find (needle , file , format ):
100
+ """ Find a line in a yaml file. """
101
+ found = find_in_file (needle , file , format )
102
+ click .echo (found )
103
+
104
+
105
+ def find_in_file (needle , file , format ):
106
+ # read yaml file
107
+ fp = open (file )
108
+ filecontents = fp .read ()
109
+ fp .close ()
110
+
111
+ # parse the file
112
+ data = YamlParser .load_yaml (filecontents )
113
+ try :
114
+ element = find_element (data , needle )
115
+ except exceptions .InvalidSearchStringException :
116
+ # TODO (jroovers): we should deduplicate this code. Best done by moving the core business logic
117
+ # (like find_element) out of this module into it's own module and then creating a wrapper function
118
+ # here that deals with exception handling
119
+ click .echo ("ERROR: Invalid search string '%s' for file '%s'" % (needle , file ), err = True )
120
+ exit (1 )
121
+
122
+ return apply_format (file , filecontents , element , format )
123
+
124
+
125
+ def apply_format (file , filecontents , element , format ):
126
+ """ Given a yaml element and yamlpal format string, return the interpolated string.
127
+ We currently support the following placeholders:
128
+ - %{key} -> key of the yaml element (index if you are accessing a list)
129
+ - %{value} -> value of the yaml element
130
+ - %{literal} -> the string corresponding to the yaml element as it literally occurs in the file
131
+ - %{linenr} -> line number on which the yaml element is found
132
+ - %{file} -> name of the file in which the yaml element is found
133
+ """
134
+
135
+ result = format .replace ("%{key}" , str (element .key ))
136
+ result = result .replace ("%{value}" , str (element ))
137
+ result = result .replace ("%{linenr}" , str (element .line ))
138
+ result = result .replace ("%{file}" , file )
139
+
140
+ # check whether literal occurs before splitting the file, since it's a more expensive operation
141
+ if "%{literal}" in format :
142
+ lines = filecontents .split ("\n " , element .line + 1 ) # don't split more than required
143
+ result = result .replace ("%{literal}" , lines [element .line ])
144
+
145
+ return result
146
+
147
+
70
148
def insert_in_file (needle , newcontent , file , inline ):
71
149
# read yaml file
72
150
fp = open (file )
@@ -123,6 +201,7 @@ def find_element(yaml_dict, search_str):
123
201
# dictionary or list.
124
202
try :
125
203
node .line
204
+ node .key = parsed_parts [- 1 ] # add the last parsed key as the node's key
126
205
except AttributeError :
127
206
click .echo ("ERROR: Path exists but not specific enough (%s)." % search_str , err = True )
128
207
exit (1 )
0 commit comments