55from __future__ import absolute_import , division , print_function , unicode_literals
66
77"""
8- neofile.py — CLI for the PyNeoFile format (.neo).
9- Uses the pyneofile wrappers (which call into pyneoarc_alt) and prefers pyneofile.ini.
10- Supports converting from zip/tar (stdlib), and rar/7z when optional libs are installed.
8+ pyneofile_cli.py — CLI for the PyNeoFile format (.neo).
9+
10+ New:
11+ - Input '-' for list/validate/extract/repack/convert reads archive bytes from stdin.
12+ - Output '-' for create/repack/convert writes archive bytes to stdout.
13+ - Extract with '-o -' streams a TAR archive to stdout (use: `... -e -i in.neo -o - > out.tar`).
1114"""
1215
13- import os , sys , argparse , tempfile
16+ import os , sys , argparse , tempfile , tarfile , io , base64
1417import pyneofile as N
1518
1619__program_name__ = "pyneofile"
17- __version__ = "0.1.0"
20+ __version__ = "0.2.0"
21+
22+ def _stdout_bin ():
23+ return getattr (sys .stdout , "buffer" , sys .stdout )
24+
25+ def _stdin_bin ():
26+ return getattr (sys .stdin , "buffer" , sys .stdin )
1827
1928def _build_formatspecs_from_args (args ):
20- # Allow explicit override; otherwise let wrappers auto-load pyneofile.ini
2129 if args .format is None or args .format .lower () == "auto" :
2230 return None
2331 return {
@@ -30,10 +38,9 @@ def _build_formatspecs_from_args(args):
3038
3139def _convert_or_fail (infile , outpath , formatspecs , checksum , compression , level ):
3240 try :
33- N .convert_foreign_to_neo (infile , outpath , formatspecs = formatspecs ,
34- checksumtypes = (checksum , checksum , checksum ),
35- compression = compression , compression_level = level )
36- return True
41+ return N .convert_foreign_to_neo (infile , outpath , formatspecs = formatspecs ,
42+ checksumtypes = (checksum , checksum , checksum ),
43+ compression = compression , compression_level = level )
3744 except RuntimeError as e :
3845 msg = str (e )
3946 if "rarfile" in msg .lower ():
@@ -42,20 +49,45 @@ def _convert_or_fail(infile, outpath, formatspecs, checksum, compression, level)
4249 sys .stderr .write ("error: 7z support requires 'py7zr'. Install via: pip install py7zr\n " )
4350 else :
4451 sys .stderr .write ("convert error: %s\n " % msg )
45- return False
52+ return None
4653 except Exception as e :
4754 sys .stderr .write ("convert error: %s\n " % e )
48- return False
55+ return None
56+
57+ def _emit_tar_stream_from_array (arr , outfp ):
58+ """Write a tar stream to outfp from the parsed archive array (no re-compress)."""
59+ tf = tarfile .open (fileobj = outfp , mode = 'w|' ) # stream mode
60+ try :
61+ for ent in arr ['ffilelist' ]:
62+ name = ent ['fname' ].lstrip ('./' )
63+ if ent ['ftype' ] == 5 :
64+ ti = tarfile .TarInfo (name = name .rstrip ('/' ) + '/' )
65+ ti .type = tarfile .DIRTYPE
66+ ti .mode = ent .get ('fmode' , 0o755 ) & 0o777
67+ ti .mtime = ent .get ('fmtime' , 0 )
68+ ti .size = 0
69+ tf .addfile (ti )
70+ else :
71+ data = ent .get ('fcontent' ) or b''
72+ bio = io .BytesIO (data )
73+ ti = tarfile .TarInfo (name = name )
74+ ti .type = tarfile .REGTYPE
75+ ti .mode = ent .get ('fmode' , 0o644 ) & 0o777
76+ ti .mtime = ent .get ('fmtime' , 0 )
77+ ti .size = len (data )
78+ tf .addfile (ti , fileobj = bio )
79+ finally :
80+ tf .close ()
4981
5082def main (argv = None ):
5183 p = argparse .ArgumentParser (description = "PyNeoFile (.neo) archiver" , add_help = True )
5284 p .add_argument ("-V" ,"--version" , action = "version" , version = __program_name__ + " " + __version__ )
5385
54- p .add_argument ("-i" ,"--input" , nargs = "+" , required = True , help = "Input files/dirs or archive file" )
55- p .add_argument ("-o" ,"--output" , default = None , help = "Output file or directory" )
86+ p .add_argument ("-i" ,"--input" , nargs = "+" , required = True , help = "Input files/dirs or archive file ('-' = stdin for archive bytes or newline-separated paths with -c) " )
87+ p .add_argument ("-o" ,"--output" , default = None , help = "Output file or directory ('-'=stdout for archive bytes; on -e streams a TAR) " )
5688
5789 p .add_argument ("-c" ,"--create" , action = "store_true" , help = "Create a .neo archive from inputs" )
58- p .add_argument ("-e" ,"--extract" , action = "store_true" , help = "Extract an archive to --output" )
90+ p .add_argument ("-e" ,"--extract" , action = "store_true" , help = "Extract an archive to --output (or stream TAR to stdout with -o -) " )
5991 p .add_argument ("-r" ,"--repack" , action = "store_true" , help = "Repack an archive (change compression)" )
6092 p .add_argument ("-l" ,"--list" , action = "store_true" , help = "List entries" )
6193 p .add_argument ("-v" ,"--validate" , action = "store_true" , help = "Validate checksums" )
@@ -70,6 +102,7 @@ def main(argv=None):
70102 p .add_argument ("-C" ,"--checksum" , default = "crc32" , help = "Checksum algorithm" )
71103 p .add_argument ("-s" ,"--skipchecksum" , action = "store_true" , help = "Skip checks while reading" )
72104 p .add_argument ("-d" ,"--verbose" , action = "store_true" , help = "Verbose listing" )
105+ p .add_argument ("-T" ,"--text" , action = "store_true" , help = "Treat -i - as newline-separated path list when used with -c/--create" )
73106
74107 args = p .parse_args (argv )
75108
@@ -80,52 +113,97 @@ def main(argv=None):
80113 level = None if args .level in (None , "" ,) else int (args .level )
81114 checksum = args .checksum
82115
116+ # Determine active action
117+ actions = ["create" ,"extract" ,"repack" ,"list" ,"validate" ]
118+ active = next ((a for a in actions if getattr (args , a )), None )
119+ if not active :
120+ p .error ("one of --create/--extract/--repack/--list/--validate is required" )
121+
122+ # Helper: read archive bytes from stdin for non-create ops
123+ def _maybe_archive_bytes ():
124+ if infile0 == '-' :
125+ return _stdin_bin ().read ()
126+ return None
127+
83128 if args .create :
129+ if infile0 == '-' and not args .text :
130+ # read newline-separated paths from stdin
131+ items = [line .strip () for line in sys .stdin if line .strip () and not line .startswith ('#' )]
132+ else :
133+ items = inputs
134+
84135 if args .convert :
85- if not args .output : p .error ("--output is required" )
86- ok = _convert_or_fail (infile0 , args .output , formatspecs , checksum , compression , level )
87- return 0 if ok else 1
88- if not args .output : p .error ("--output is required" )
89- N .pack_neo (inputs , args .output , formatspecs = formatspecs ,
90- checksumtypes = (checksum , checksum , checksum ),
91- compression = compression , compression_level = level )
92- if args .verbose : sys .stderr .write ("created: %s\n " % args .output )
136+ if not args .output :
137+ p .error ("--output is required (use '-' to stream to stdout)" )
138+ data = _convert_or_fail (infile0 , (None if args .output == '-' else args .output ),
139+ formatspecs , checksum , compression , level )
140+ if data is None :
141+ return 1
142+ if args .output == '-' :
143+ _stdout_bin ().write (data )
144+ return 0
145+
146+ if not args .output :
147+ p .error ("--output is required for --create (use '-' to stream)" )
148+ out_bytes = (args .output == '-' )
149+ if out_bytes :
150+ data = N .pack_neo (items , None , formatspecs = formatspecs ,
151+ checksumtypes = (checksum , checksum , checksum ),
152+ compression = compression , compression_level = level )
153+ _stdout_bin ().write (data )
154+ else :
155+ N .pack_neo (items , args .output , formatspecs = formatspecs ,
156+ checksumtypes = (checksum , checksum , checksum ),
157+ compression = compression , compression_level = level )
158+ if args .verbose : sys .stderr .write ("created: %s\n " % args .output )
93159 return 0
94160
95161 if args .repack :
162+ src = _maybe_archive_bytes () or infile0
96163 if args .convert :
97- if not args .output : p .error ("--output is required" )
98- ok = _convert_or_fail (infile0 , args .output , formatspecs , checksum , compression , level )
99- return 0 if ok else 1
100- if not args .output : p .error ("--output is required" )
101- N .repack_neo (infile0 , args .output , formatspecs = formatspecs ,
102- checksumtypes = (checksum , checksum , checksum ),
103- compression = compression , compression_level = level )
104- if args .verbose : sys .stderr .write ("repacked: %s\n " % args .output )
164+ if not args .output :
165+ p .error ("--output is required (use '-' to stream)" )
166+ data = _convert_or_fail (src , (None if args .output == '-' else args .output ),
167+ formatspecs , checksum , compression , level )
168+ if data is None :
169+ return 1
170+ if args .output == '-' :
171+ _stdout_bin ().write (data )
172+ return 0
173+
174+ if not args .output :
175+ p .error ("--output is required for --repack (use '-' to stream)" )
176+ if args .output == '-' :
177+ data = N .repack_neo (src , None , formatspecs = formatspecs ,
178+ checksumtypes = (checksum , checksum , checksum ),
179+ compression = compression , compression_level = level )
180+ _stdout_bin ().write (data )
181+ else :
182+ N .repack_neo (src , args .output , formatspecs = formatspecs ,
183+ checksumtypes = (checksum , checksum , checksum ),
184+ compression = compression , compression_level = level )
185+ if args .verbose : sys .stderr .write ("repacked: %s -> %s\n " % (('<stdin>' if infile0 == '-' else infile0 ), args .output ))
105186 return 0
106187
107188 if args .extract :
189+ src = _maybe_archive_bytes () or infile0
190+ if args .output in (None , '.' ) and infile0 == '-' :
191+ # default would attempt to mkdir '.'; fine
192+ pass
193+ if args .output == '-' :
194+ # stream TAR to stdout
195+ arr = N .archive_to_array_neo (src , formatspecs = formatspecs , listonly = False ,
196+ skipchecksum = args .skipchecksum , uncompress = True )
197+ _emit_tar_stream_from_array (arr , _stdout_bin ())
198+ return 0
108199 outdir = args .output or "."
109- if args .convert :
110- tmp_arc = os .path .join (tempfile .gettempdir (), "pyneofile_convert.neo" )
111- ok = _convert_or_fail (infile0 , tmp_arc , formatspecs , checksum , compression , level )
112- if not ok : return 1
113- use = tmp_arc
114- else :
115- use = infile0
116- N .unpack_neo (use , outdir , formatspecs = formatspecs , skipchecksum = args .skipchecksum , uncompress = True )
200+ N .unpack_neo (src , outdir , formatspecs = formatspecs , skipchecksum = args .skipchecksum , uncompress = True )
117201 if args .verbose : sys .stderr .write ("extracted → %s\n " % outdir )
118202 return 0
119203
120204 if args .list :
121- if args .convert :
122- tmp_arc = os .path .join (tempfile .gettempdir (), "pyneofile_convert.neo" )
123- ok = _convert_or_fail (infile0 , tmp_arc , formatspecs , checksum , compression , level )
124- if not ok : return 1
125- use = tmp_arc
126- else :
127- use = infile0
128- names = N .archivefilelistfiles_neo (use , formatspecs = formatspecs , advanced = args .verbose , include_dirs = True )
205+ src = _maybe_archive_bytes () or infile0
206+ names = N .archivefilelistfiles_neo (src , formatspecs = formatspecs , advanced = args .verbose , include_dirs = True )
129207 if not args .verbose :
130208 for n in names : sys .stdout .write (n + "\n " )
131209 else :
@@ -136,14 +214,8 @@ def main(argv=None):
136214 return 0
137215
138216 if args .validate :
139- if args .convert :
140- tmp_arc = os .path .join (tempfile .gettempdir (), "pyneofile_convert.neo" )
141- ok = _convert_or_fail (infile0 , tmp_arc , formatspecs , checksum , compression , level )
142- if not ok : return 1
143- use = tmp_arc
144- else :
145- use = infile0
146- ok , details = N .archivefilevalidate_neo (use , formatspecs = formatspecs , verbose = args .verbose , return_details = True )
217+ src = _maybe_archive_bytes () or infile0
218+ ok , details = N .archivefilevalidate_neo (src , formatspecs = formatspecs , verbose = args .verbose , return_details = True )
147219 if not args .verbose :
148220 sys .stdout .write ("valid: %s\n " % ("yes" if ok else "no" ))
149221 else :
0 commit comments