1
1
#!/usr/bin/env python
2
2
# -*- coding: utf-8 -*-
3
3
4
- from __future__ import print_function
5
-
6
4
import sys
7
5
import os .path
8
6
import json
9
7
import jsonpatch
8
+ import tempfile
10
9
import argparse
11
10
12
11
@@ -15,9 +14,14 @@ parser = argparse.ArgumentParser(
15
14
parser .add_argument ('ORIGINAL' , type = argparse .FileType ('r' ),
16
15
help = 'Original file' )
17
16
parser .add_argument ('PATCH' , type = argparse .FileType ('r' ),
18
- help = 'Patch file' )
17
+ nargs = '?' , default = sys .stdin ,
18
+ help = 'Patch file (read from stdin if omitted)' )
19
19
parser .add_argument ('--indent' , type = int , default = None ,
20
20
help = 'Indent output by n spaces' )
21
+ parser .add_argument ('-b' , '--backup' , action = 'store_true' ,
22
+ help = 'Back up ORIGINAL if modifying in-place' )
23
+ parser .add_argument ('-i' , '--in-place' , action = 'store_true' ,
24
+ help = 'Modify ORIGINAL in-place instead of to stdout' )
21
25
parser .add_argument ('-v' , '--version' , action = 'version' ,
22
26
version = '%(prog)s ' + jsonpatch .__version__ )
23
27
@@ -35,7 +39,67 @@ def patch_files():
35
39
doc = json .load (args .ORIGINAL )
36
40
patch = json .load (args .PATCH )
37
41
result = jsonpatch .apply_patch (doc , patch )
38
- print (json .dumps (result , indent = args .indent ))
42
+
43
+ if args .in_place :
44
+ dirname = os .path .abspath (os .path .dirname (args .ORIGINAL .name ))
45
+
46
+ try :
47
+ # Attempt to replace the file atomically. We do this by
48
+ # creating a temporary file in the same directory as the
49
+ # original file so we can atomically move the new file over
50
+ # the original later. (This is done in the same directory
51
+ # because atomic renames do not work across mount points.)
52
+
53
+ fd , pathname = tempfile .mkstemp (dir = dirname )
54
+ fp = os .fdopen (fd , 'w' )
55
+ atomic = True
56
+
57
+ except OSError :
58
+ # We failed to create the temporary file for an atomic
59
+ # replace, so fall back to non-atomic mode by backing up
60
+ # the original (if desired) and writing a new file.
61
+
62
+ if args .backup :
63
+ os .rename (args .ORIGINAL .name , args .ORIGINAL .name + '.orig' )
64
+ fp = open (args .ORIGINAL .name , 'w' )
65
+ atomic = False
66
+
67
+ else :
68
+ # Since we're not replacing the original file in-place, write
69
+ # the modified JSON to stdout instead.
70
+
71
+ fp = sys .stdout
72
+
73
+ # By this point we have some sort of file object we can write the
74
+ # modified JSON to.
75
+
76
+ json .dump (result , fp , indent = args .indent )
77
+ fp .write ('\n ' )
78
+
79
+ if args .in_place :
80
+ # Close the new file. If we aren't replacing atomically, this
81
+ # is our last step, since everything else is already in place.
82
+
83
+ fp .close ()
84
+
85
+ if atomic :
86
+ try :
87
+ # Complete the atomic replace by linking the original
88
+ # to a backup (if desired), fixing up the permissions
89
+ # on the temporary file, and moving it into place.
90
+
91
+ if args .backup :
92
+ os .link (args .ORIGINAL .name , args .ORIGINAL .name + '.orig' )
93
+ os .chmod (pathname , os .stat (args .ORIGINAL .name ).st_mode )
94
+ os .rename (pathname , args .ORIGINAL .name )
95
+
96
+ except OSError :
97
+ # In the event we could not actually do the atomic
98
+ # replace, unlink the original to move it out of the
99
+ # way and finally move the temporary file into place.
100
+
101
+ os .unlink (args .ORIGINAL .name )
102
+ os .rename (pathname , args .ORIGINAL .name )
39
103
40
104
41
105
if __name__ == "__main__" :
0 commit comments