Search code examples
fish

How do I use argv with a wildcard?


I'm trying to write a function which will change the extension for all matching files in the current directory:

function chext
  if count $args -eq 2 > /dev/null
    for f in (ls *$argv[1])
      mv (basename $f $argv[2]) (basename $f $argv[1])
    end
  else
    echo "Error: use the following syntax"
    echo "chext oldext newext"
  end
end

I keep getting this output though:

(*master) λ chext markdown txt
No matches for wildcard '*$argv[1]'.  (Tip: empty matches are allowed in 'set', 'count', 'for'.)
~/.config/fish/config.fish (line 1): ls *$argv[1]
                                        ^
in command substitution
        called on line 60 of file ~/.config/fish/config.fish

in function 'chext'
        called on standard input
        with parameter list 'markdown txt'

I know there must be a way to interpolate the variable and keep * as a wildcard, but I can't figure it out. I have tried using (string join '*' $argv[1]), but it turned the wildcard into a string.

I did get this to almost work:

function chext
  if count $args -eq 2 > /dev/null
    for f in (ls (string join * $argv[1]))
      mv (basename $f $argv[2]) (basename $f $argv[1])
    end
  else
    echo "Error: use the following syntax"
    echo "chext oldext newext"
  end
end

It removed the extensions from all of my files but didn't add the new one, then gave me an error which was all of the file names combined and said file name too long.

UPDATE:

I'm sure there is a way to get the correct ls working, but I did get this working successfully:

if count $args -eq 2 > /dev/null
    for f in (ls)
      mv $f (string replace -r \.$argv[1]\$ .$argv[2] (basename $f))
    end
  else
    echo "Error: use the following syntax"
    echo "chext oldext newext"
  end
end

Solution

  • The answer is in the message:

    No matches for wildcard '*$argv[1]'. (Tip: empty matches are allowed in 'set', 'count', 'for'.)

    That means

    for i in *argv[1]
    

    works, as does

    set -l expanded *argv[1]
    

    If you try to launch any other command with a glob that doesn't match anything, fish will skip the execution and print an error.


    Your function has a number of other issues, allow me to go through them one by one.

    if count $args -eq 2 > /dev/null

    count does not take the "-eq 2" argument. It will return 0 (i.e. true) whenever it is given an argument, so this will always be true.

    What you tried to do was if test (count $args) -eq 2.

    What also works (and what I happen to like more) is if set -q argv[2]

    (also, it's "argv", not "args").

    for f in (ls *$argv[1])

    Please do not use the output of ls. It doesn't have as much problems in fish as it does in bash (where it'll break once a file has a space, tab or newline in the name), but it's unnecessary (it launches an additional process) and still has an issue - it'll break when a file has a newline in its name.

    Just do for f in *$argv[1], which will also nicely solve your question.

     mv (basename $f $argv[2]) (basename $f $argv[1])
    

    I'm not sure what basename you are using here, but mine just removes a suffix. So if you did chext .opus .ogg, this would execute

     mv (basename song.opus .ogg) (basename song.opus .opus)
    

    which would resolve to

     mv song.opus song
    

    Personally, I'd use string replace for this. Something like

    mv $f (string replace -- $argv[1] $argv[2] $f)
    

    (Note: This would do the wrong thing if the extension string appears before, e.g. if you had a file called "f.ogg-banana.ogg" only the first ".ogg" would be changed. You might want to use regular expressions for this: string replace -r -- "$argv[1]\$" $argv[2] $f)