@@ -3,7 +3,7 @@ import { foregroundChild } from 'foreground-child';
33import { existsSync } from 'fs' ;
44import { jack } from 'jackspeak' ;
55import { loadPackageJson } from 'package-json-from-dist' ;
6- import { join } from 'path' ;
6+ import { basename , join } from 'path' ;
77import { globStream } from './index.js' ;
88const { version } = loadPackageJson ( import . meta. url , '../package.json' ) ;
99const j = jack ( {
@@ -30,6 +30,50 @@ const j = jack({
3030 description : `If no positional arguments are provided, glob will use
3131 this pattern` ,
3232 } ,
33+ } )
34+ . flag ( {
35+ shell : {
36+ default : false ,
37+ description : `Interpret the command as a shell command by passing it
38+ to the shell, with all matched filesystem paths appended,
39+ **even if this cannot be done safely**.
40+
41+ This is **not** unsafe (and usually unnecessary) when using
42+ the known Unix shells sh, bash, zsh, and fish, as these can
43+ all be executed in such a way as to pass positional
44+ arguments safely.
45+
46+ **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED,
47+ because a path like \`'some/path/\\$\\(cmd)'\` will be
48+ executed by the shell.
49+
50+ If you do have positional arguments that you wish to pass to
51+ the command ahead of the glob pattern matches, use the
52+ \`--cmd-arg\`/\`-g\` option instead.
53+
54+ The next major release of glob will fully remove the ability
55+ to use this option unsafely.` ,
56+ } ,
57+ } )
58+ . optList ( {
59+ 'cmd-arg' : {
60+ short : 'g' ,
61+ hint : 'arg' ,
62+ default : [ ] ,
63+ description : `Pass the provided values to the supplied command, ahead of
64+ the glob matches.
65+
66+ For example, the command:
67+
68+ glob -c echo -g"hello" -g"world" *.txt
69+
70+ might output:
71+
72+ hello world a.txt b.txt
73+
74+ This is a safer (and future-proof) alternative than putting
75+ positional arguments in the \`-c\`/\`--cmd\` option.` ,
76+ } ,
3377} )
3478 . flag ( {
3579 all : {
@@ -74,7 +118,7 @@ const j = jack({
74118 description : `Always resolve to posix style paths, using '/' as the
75119 directory separator, even on Windows. Drive letter
76120 absolute matches on Windows will be expanded to their
77- full resolved UNC maths , eg instead of 'C:\\foo\\bar',
121+ full resolved UNC paths , eg instead of 'C:\\foo\\bar',
78122 it will expand to '//?/C:/foo/bar'.
79123 ` ,
80124 } ,
@@ -209,57 +253,89 @@ const j = jack({
209253 description : `Output a huge amount of noisy debug information about
210254 patterns as they are parsed and used to match files.` ,
211255 } ,
212- } )
213- . flag ( {
256+ version : {
257+ short : 'V' ,
258+ description : `Output the version (${ version } )` ,
259+ } ,
214260 help : {
215261 short : 'h' ,
216262 description : 'Show this usage information' ,
217263 } ,
218264} ) ;
219265try {
220266 const { positionals, values } = j . parse ( ) ;
221- if ( values . help ) {
267+ const { cmd, shell, all, default : def , version : showVersion , help, absolute, cwd, dot, 'dot-relative' : dotRelative , follow, ignore, 'match-base' : matchBase , 'max-depth' : maxDepth , mark, nobrace, nocase, nodir, noext, noglobstar, platform, realpath, root, stat, debug, posix, 'cmd-arg' : cmdArg , } = values ;
268+ if ( showVersion ) {
269+ console . log ( version ) ;
270+ process . exit ( 0 ) ;
271+ }
272+ if ( help ) {
222273 console . log ( j . usage ( ) ) ;
223274 process . exit ( 0 ) ;
224275 }
225- if ( positionals . length === 0 && ! values . default )
276+ //const { shell, help } = values
277+ if ( positionals . length === 0 && ! def )
226278 throw 'No patterns provided' ;
227- if ( positionals . length === 0 && values . default )
228- positionals . push ( values . default ) ;
229- const patterns = values . all ? positionals : positionals . filter ( p => ! existsSync ( p ) ) ;
230- const matches = values . all ?
231- [ ]
232- : positionals . filter ( p => existsSync ( p ) ) . map ( p => join ( p ) ) ;
279+ if ( positionals . length === 0 && def )
280+ positionals . push ( def ) ;
281+ const patterns = all ? positionals : positionals . filter ( p => ! existsSync ( p ) ) ;
282+ const matches = all ? [ ] : positionals . filter ( p => existsSync ( p ) ) . map ( p => join ( p ) ) ;
233283 const stream = globStream ( patterns , {
234- absolute : values . absolute ,
235- cwd : values . cwd ,
236- dot : values . dot ,
237- dotRelative : values [ 'dot-relative' ] ,
238- follow : values . follow ,
239- ignore : values . ignore ,
240- mark : values . mark ,
241- matchBase : values [ 'match-base' ] ,
242- maxDepth : values [ 'max-depth' ] ,
243- nobrace : values . nobrace ,
244- nocase : values . nocase ,
245- nodir : values . nodir ,
246- noext : values . noext ,
247- noglobstar : values . noglobstar ,
248- platform : values . platform ,
249- realpath : values . realpath ,
250- root : values . root ,
251- stat : values . stat ,
252- debug : values . debug ,
253- posix : values . posix ,
284+ absolute,
285+ cwd,
286+ dot,
287+ dotRelative,
288+ follow,
289+ ignore,
290+ mark,
291+ matchBase,
292+ maxDepth,
293+ nobrace,
294+ nocase,
295+ nodir,
296+ noext,
297+ noglobstar,
298+ platform : platform ,
299+ realpath,
300+ root,
301+ stat,
302+ debug,
303+ posix,
254304 } ) ;
255- const cmd = values . cmd ;
256305 if ( ! cmd ) {
257306 matches . forEach ( m => console . log ( m ) ) ;
258307 stream . on ( 'data' , f => console . log ( f ) ) ;
259308 }
260309 else {
261- stream . on ( 'data' , f => matches . push ( f ) ) ;
262- stream . on ( 'end' , ( ) => foregroundChild ( cmd , matches , { shell : true } ) ) ;
310+ cmdArg . push ( ...matches ) ;
311+ stream . on ( 'data' , f => cmdArg . push ( f ) ) ;
312+ // Attempt to support commands that contain spaces and otherwise require
313+ // shell interpretation, but do NOT shell-interpret the arguments, to avoid
314+ // injections via filenames. This affordance can only be done on known Unix
315+ // shells, unfortunately.
316+ //
317+ // 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches]
318+ // 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches]
319+ // 'fish', ['-c', cmd + ' "$argv"', ...matches]
320+ const { SHELL = 'unknown' } = process . env ;
321+ const shellBase = basename ( SHELL ) ;
322+ const knownShells = [ 'sh' , 'ksh' , 'zsh' , 'bash' , 'fish' ] ;
323+ if ( ( shell || / [ " ' ] / . test ( cmd ) ) &&
324+ knownShells . includes ( shellBase ) ) {
325+ const cmdWithArgs = `${ cmd } "\$${ shellBase === 'fish' ? 'argv' : '@' } "` ;
326+ if ( shellBase !== 'fish' ) {
327+ cmdArg . unshift ( SHELL ) ;
328+ }
329+ cmdArg . unshift ( '-c' , cmdWithArgs ) ;
330+ stream . on ( 'end' , ( ) => foregroundChild ( SHELL , cmdArg ) ) ;
331+ }
332+ else {
333+ if ( shell ) {
334+ process . emitWarning ( 'The --shell option is unsafe, and will be removed. To pass ' +
335+ 'positional arguments to the subprocess, use -g/--cmd-arg instead.' , 'DeprecationWarning' , 'GLOB_SHELL' ) ;
336+ }
337+ stream . on ( 'end' , ( ) => foregroundChild ( cmd , cmdArg , { shell } ) ) ;
338+ }
263339 }
264340}
265341catch ( e ) {
0 commit comments