11

I've copied the command cd C:\foo\bar\ from PowerShell to Cygwin and sillily expected it to execute. I'm now trying to run a substitution to replace all the \ with /:

$ !!:gs/\\/\/
bash: :gs/\\/\/: substitution failed

Not sure why I'm getting substitution failed.

I also tried:

$ !!:gs/\\/q

Just to see if the replace was the problem. It's not. Now I'm curious!

Why am I getting "substitution failed"?

7
  • 2
    I have no cygwin at hand right now, but I thought, that Windows paths are working... have you tried to singe-quote the argument? I.e. cd 'C:\foo\bar'
    – mpy
    Jan 17, 2017 at 21:17
  • @mpy This actually works too! Didn't know I wasn't stuck with substitution.
    – rtf
    Jan 17, 2017 at 21:18
  • 1
    "Would be open to an answer using zsh as well". Your original !!:gs/\\/\/ does work in zsh… Jan 17, 2017 at 21:55
  • @TannerFaulkner fc -s '\'='/' -1 works under the Ubuntu LTS default bash. Let me know if it works under Cygwin too. I posted more words in the answer.
    – Hastur
    Jan 23, 2017 at 16:21
  • 1
    i think the general bash behavior here is that the backslashes are processed as escapes before the substitution happens in !!:gs/\\/\//. @Hastur 's fc -s '\'='/' -1 gets the expected behavior. this might be a bug in bash.
    – quixotic
    Jan 24, 2017 at 16:01

4 Answers 4

6

a forward slash is also the delimiter of your substitute command, so you must escape it as the replacement string so as not to end the substitution early

!!:gs/\\/\//

or more plainly as suggested in the comments, by using something besides slash as the delimiter

!!:gs|\\|/|

But this only seems to work in tcsh (the shell commonly assigned where I am), I can't get the command to work in bash as it seems escaping doesn't work on the first argument of :s

4
  • No joy :( Same error i.imgur.com/QFQR0xF.png
    – rtf
    Jan 17, 2017 at 21:02
  • 1
    Just a remark: !!:gs|\\|/ might be a little bit more readable.
    – mpy
    Jan 17, 2017 at 21:19
  • 1
    I could not get it to work in bash. The first arg of :s doen't seem to be a real regex
    – infixed
    Jan 17, 2017 at 21:40
  • This is not working Sep 15, 2020 at 7:23
5
+200

Short answer: the builtin fc (fix command) of bash

fc is the command, built-in in the bash shell, made to edit & re-execute commands of the history.
It is present on CygWin too and it works on all the Linux distributions on which I tested:

fc  -s '\'='/' -1

Some explanations

  • fc is the bash built-in command to list or edit and re-execute commands from the history list.
  • -1 will take the last command in the history. Note that even !! is defined (read from man bash) as

    !! Refer to the previous command. This is a synonym for!-1'.

  • -s to substitute a pattern with another. This time from help fc (built-in command so help):

    With the `fc -s [pat=rep ...] [command]' format, COMMAND is re-executed after the substitution OLD=NEW is performed.

    Ok they mean pat=rep instead of OLD=NEW ...

  • The patterns will be read by the shell so we have to protect them with quote: '\' and '/'.

Some words more about why you are getting "substitution failed"

It seems that for the s modifier is not (yet) implemented the substitution of the backslash character \, that is the escape one. To be sure we should see the code, for example, of the gnu version of the bash history expansion (but there was the above command to obtain what you were trying to do...so I take it lazy....).

Some notes:

  1. We are lead to think that it will work each RegEx we find working with sed, but it is not guaranteed. The backslash is the escape character of the expansion and the problem is here. Moreover the behaviour of the expansion is related with the shopt options, so we should start to see case by case...

  2. When you paste the string cd C:\Foo\Bar in your bash shell it will be expanded and it will appear for the interpreter as cd C:FooBar; in this form it will be stored in the $_ internal variable too.
    If you instead pasted cd "C:\Foo\Bar" or cd 'C:\Foo\Bar' in the $_ variable you should find C:\Foo\Bar.
    Since the History expansion is performed immediately after a complete line is read, before the shell breaks it into words, you may be tempted to start to use it with some bashism more or less plain, e.g., with some derivation from (maybe adding :p or :q, "", parsing and so on...)

    !!:0 ${_//\\/\/}
    

    This is the moment to remember that it is not safe to start to play with path and filenames, especially if they come from the windows clipboard (read in general the page Why not parse ls?, it is essentially related with the possibility to use tab, spaces and newlines as correct characters for the file names and the directory ones...).
    Moreover when you paste a text captured with the mouse, you may paste a leading space too. This may avoid that your command will finish in the history (it depends from the shell options...). If so your following !! will be a not controlled command... (see an example in another answer). This is a unneeded tangible risk.

Conclusion

History expansions introduce words from the history list into the input stream, making it easy to repeat commands, insert the arguments to a previous command into the current input line, or fix errors in previous commands quickly.

If it is not easy I start to think we are doing something wrong ;-)


Ad nauseam: a little experiment

I've enabled histverify in the shell then...

shopt -s histverify
echo C:\Foo\Bar
!!:s|C|D| {1,2}A

then I press Enter and as verified expansion I find

echo D:\Foo\Bar {1,2}A

then I press Enter again and it echoes

D:FooBar 1A 2A

This seems to indicate that the substitution failed is generated in the history expansion processed before of the Brace expansion, so first of all, and it seems to confirm that the s history modifier didn't (yet) process the substitution of the \ character as a real regex...

2

You can use sed to substitute and execute the result.

There are several possible variants for such a command, but on my bash only this one worked:

A=$(echo "!!\\" | sed 's,\\,/,g');eval $A

The \\ is added to the !! is in case the last command terminated with a backslash. Without it, the shell will get confused and ask for the continuation of the line.

You could also add this as a bash function in the file ~/.bashrc :

function slash {
   A=$(history | tail -n 2 | head -n 1 | cut -c 8- | sed 's,\\,/,g')
   eval $A
}

Here is how this works on my Bash on Windows :

bash

To explain how the A= command assigns the right value to the shell variable A:

  • tail takes the last two lines from the command history, which gives the lines of cd \mnt\c followed by slash itself. The part of history | tail -n 2 may also be written as history 2.
  • head take the first line which is cd \mnt\c
  • cut cuts out the line-number supplied by history
  • sed replaces all anti-slashes by slashes
  • eval executes the contents of the variable A
2
  • Hi Harry. Sorry to say that on my GNU bash version 4.3.11(1) the A=$(echo "!!\\" | sed 's,\\,/,g');eval $A doesn't work when the command terminated with a backslash. You are forced to add a space, e.g. to C:\Foo\Bar\ else, as you said, the shell will wait for the continuation of the line. You can also press enter twice. In the first case you obtain C:/Foo/Bar / (with a space before the /; in the second it will generate an error. The function works fine.
    – Hastur
    Jan 23, 2017 at 16:41
  • I wrote the function because (1) It's much easier to use, and (2) The one-liner seemed too implementation-dependent.
    – harrymc
    Jan 23, 2017 at 19:26
-1

I don't think you can do this. You need to copy/past again.

The issue is not related to slash beeing also the substitute command separator, since substitute command accept any separator. The issue is about the back-slashes to be substituted, which have been already treated as simple useless escapements of character on their right.

Hence, when you call the substitute command, backslash is already vanished from the source. Just try to re-call the command as is (!!). Instead of printing the exact same command including c:\foo, it will execute the command minus already-processed backslashes which result in c:foo.

And obviously you can not find nor replace a missing character. Trying to do so will fire an error.

1
  • 1
    Well. That is finally wrong since this test actually works: echo c:\Foo followed by !!:gs,F,\\f,. But Substituting the backslash escape itself appears to be a pain.
    – A. Loiseau
    Jan 17, 2017 at 21:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .