@@ -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'
88
99const { version } = loadPackageJson ( import . meta. url , '../package.json' )
@@ -35,6 +35,50 @@ const j = jack({
3535 this pattern` ,
3636 } ,
3737 } )
38+ . flag ( {
39+ shell : {
40+ default : false ,
41+ description : `Interpret the command as a shell command by passing it
42+ to the shell, with all matched filesystem paths appended,
43+ **even if this cannot be done safely**.
44+
45+ This is **not** unsafe (and usually unnecessary) when using
46+ the known Unix shells sh, bash, zsh, and fish, as these can
47+ all be executed in such a way as to pass positional
48+ arguments safely.
49+
50+ **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED,
51+ because a path like \`'some/path/\\$\\(cmd)'\` will be
52+ executed by the shell.
53+
54+ If you do have positional arguments that you wish to pass to
55+ the command ahead of the glob pattern matches, use the
56+ \`--cmd-arg\`/\`-g\` option instead.
57+
58+ The next major release of glob will fully remove the ability
59+ to use this option unsafely.` ,
60+ } ,
61+ } )
62+ . optList ( {
63+ 'cmd-arg' : {
64+ short : 'g' ,
65+ hint : 'arg' ,
66+ default : [ ] ,
67+ description : `Pass the provided values to the supplied command, ahead of
68+ the glob matches.
69+
70+ For example, the command:
71+
72+ glob -c echo -g"hello" -g"world" *.txt
73+
74+ might output:
75+
76+ hello world a.txt b.txt
77+
78+ This is a safer (and future-proof) alternative than putting
79+ positional arguments in the \`-c\`/\`--cmd\` option.` ,
80+ } ,
81+ } )
3882 . flag ( {
3983 all : {
4084 short : 'A' ,
@@ -78,7 +122,7 @@ const j = jack({
78122 description : `Always resolve to posix style paths, using '/' as the
79123 directory separator, even on Windows. Drive letter
80124 absolute matches on Windows will be expanded to their
81- full resolved UNC maths , eg instead of 'C:\\foo\\bar',
125+ full resolved UNC paths , eg instead of 'C:\\foo\\bar',
82126 it will expand to '//?/C:/foo/bar'.
83127 ` ,
84128 } ,
@@ -215,8 +259,10 @@ const j = jack({
215259 description : `Output a huge amount of noisy debug information about
216260 patterns as they are parsed and used to match files.` ,
217261 } ,
218- } )
219- . flag ( {
262+ version : {
263+ short : 'V' ,
264+ description : `Output the version (${ version } )` ,
265+ } ,
220266 help : {
221267 short : 'h' ,
222268 description : 'Show this usage information' ,
@@ -225,50 +271,113 @@ const j = jack({
225271
226272try {
227273 const { positionals, values } = j . parse ( )
228- if ( values . help ) {
274+ const {
275+ cmd,
276+ shell,
277+ all,
278+ default : def ,
279+ version : showVersion ,
280+ help,
281+ absolute,
282+ cwd,
283+ dot,
284+
285+ 'dot-relative' : dotRelative ,
286+ follow,
287+ ignore,
288+ 'match-base' : matchBase ,
289+ 'max-depth' : maxDepth ,
290+ mark,
291+ nobrace,
292+ nocase,
293+ nodir,
294+ noext,
295+ noglobstar,
296+ platform,
297+ realpath,
298+ root,
299+ stat,
300+ debug,
301+ posix,
302+ 'cmd-arg' : cmdArg ,
303+ } = values
304+ if ( showVersion ) {
305+ console . log ( version )
306+ process . exit ( 0 )
307+ }
308+ if ( help ) {
229309 console . log ( j . usage ( ) )
230310 process . exit ( 0 )
231311 }
232- if ( positionals . length === 0 && ! values . default )
233- throw 'No patterns provided'
234- if ( positionals . length === 0 && values . default )
235- positionals . push ( values . default )
312+ //const { shell, help } = values
313+ if ( positionals . length === 0 && ! def ) throw 'No patterns provided'
314+ if ( positionals . length === 0 && def ) positionals . push ( def )
236315 const patterns =
237- values . all ? positionals : positionals . filter ( p => ! existsSync ( p ) )
316+ all ? positionals : positionals . filter ( p => ! existsSync ( p ) )
238317 const matches =
239- values . all ?
240- [ ]
241- : positionals . filter ( p => existsSync ( p ) ) . map ( p => join ( p ) )
318+ all ? [ ] : positionals . filter ( p => existsSync ( p ) ) . map ( p => join ( p ) )
319+
242320 const stream = globStream ( patterns , {
243- absolute : values . absolute ,
244- cwd : values . cwd ,
245- dot : values . dot ,
246- dotRelative : values [ 'dot-relative' ] ,
247- follow : values . follow ,
248- ignore : values . ignore ,
249- mark : values . mark ,
250- matchBase : values [ 'match-base' ] ,
251- maxDepth : values [ 'max-depth' ] ,
252- nobrace : values . nobrace ,
253- nocase : values . nocase ,
254- nodir : values . nodir ,
255- noext : values . noext ,
256- noglobstar : values . noglobstar ,
257- platform : values . platform as undefined | NodeJS . Platform ,
258- realpath : values . realpath ,
259- root : values . root ,
260- stat : values . stat ,
261- debug : values . debug ,
262- posix : values . posix ,
321+ absolute,
322+ cwd,
323+ dot,
324+ dotRelative,
325+ follow,
326+ ignore,
327+ mark,
328+ matchBase,
329+ maxDepth,
330+ nobrace,
331+ nocase,
332+ nodir,
333+ noext,
334+ noglobstar,
335+ platform : platform as undefined | NodeJS . Platform ,
336+ realpath,
337+ root,
338+ stat,
339+ debug,
340+ posix,
263341 } )
264342
265- const cmd = values . cmd
266343 if ( ! cmd ) {
267344 matches . forEach ( m => console . log ( m ) )
268345 stream . on ( 'data' , f => console . log ( f ) )
269346 } else {
270- stream . on ( 'data' , f => matches . push ( f ) )
271- stream . on ( 'end' , ( ) => foregroundChild ( cmd , matches , { shell : true } ) )
347+ cmdArg . push ( ...matches )
348+ stream . on ( 'data' , f => cmdArg . push ( f ) )
349+ // Attempt to support commands that contain spaces and otherwise require
350+ // shell interpretation, but do NOT shell-interpret the arguments, to avoid
351+ // injections via filenames. This affordance can only be done on known Unix
352+ // shells, unfortunately.
353+ //
354+ // 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches]
355+ // 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches]
356+ // 'fish', ['-c', cmd + ' "$argv"', ...matches]
357+ const { SHELL = 'unknown' } = process . env
358+ const shellBase = basename ( SHELL )
359+ const knownShells = [ 'sh' , 'ksh' , 'zsh' , 'bash' , 'fish' ]
360+ if (
361+ ( shell || / [ " ' ] / . test ( cmd ) ) &&
362+ knownShells . includes ( shellBase )
363+ ) {
364+ const cmdWithArgs = `${ cmd } "\$${ shellBase === 'fish' ? 'argv' : '@' } "`
365+ if ( shellBase !== 'fish' ) {
366+ cmdArg . unshift ( SHELL )
367+ }
368+ cmdArg . unshift ( '-c' , cmdWithArgs )
369+ stream . on ( 'end' , ( ) => foregroundChild ( SHELL , cmdArg ) )
370+ } else {
371+ if ( shell ) {
372+ process . emitWarning (
373+ 'The --shell option is unsafe, and will be removed. To pass ' +
374+ 'positional arguments to the subprocess, use -g/--cmd-arg instead.' ,
375+ 'DeprecationWarning' ,
376+ 'GLOB_SHELL' ,
377+ )
378+ }
379+ stream . on ( 'end' , ( ) => foregroundChild ( cmd , cmdArg , { shell } ) )
380+ }
272381 }
273382} catch ( e ) {
274383 console . error ( j . usage ( ) )
0 commit comments