From 2d5ed23ca12db5c432bda8ff3979a2f65243579b Mon Sep 17 00:00:00 2001
From: Vidar Holen <spam@vidarholen.net>
Date: Sat, 13 Jan 2018 16:39:59 -0800
Subject: [PATCH] Warn about cp/mv/ln with a single argument. Fixes #1080.

---
 ShellCheck/ASTLib.hs          |  1 +
 ShellCheck/Analytics.hs       |  1 +
 ShellCheck/Checks/Commands.hs | 37 +++++++++++++++++++++++++++++++++++
 3 files changed, 39 insertions(+)

diff --git a/ShellCheck/ASTLib.hs b/ShellCheck/ASTLib.hs
index 635acf0..dc58a12 100644
--- a/ShellCheck/ASTLib.hs
+++ b/ShellCheck/ASTLib.hs
@@ -112,6 +112,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
 getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
 
 -- Get all flags in a GNU way, up until --
+getAllFlags :: Token -> [(Token, String)]
 getAllFlags = getFlagsUntil (== "--")
 -- Get all flags in a BSD way, up until first non-flag argument or --
 getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs
index ff00922..4bc3d21 100644
--- a/ShellCheck/Analytics.hs
+++ b/ShellCheck/Analytics.hs
@@ -845,6 +845,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
 prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
 prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
 prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
+prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
 checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
        err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
 checkUnquotedN _ _ = return ()
diff --git a/ShellCheck/Checks/Commands.hs b/ShellCheck/Checks/Commands.hs
index c02e8be..63d3258 100644
--- a/ShellCheck/Checks/Commands.hs
+++ b/ShellCheck/Checks/Commands.hs
@@ -88,6 +88,7 @@ commandChecks = [
     ,checkWhileGetoptsCase
     ,checkCatastrophicRm
     ,checkLetUsage
+    ,checkMvArguments, checkCpArguments, checkLnArguments
     ]
 
 buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -850,5 +851,41 @@ checkLetUsage = CommandCheck (Exactly "let") f
     f t = whenShell [Bash,Ksh] $ do
         style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
 
+
+missingDestination handler token = do
+    case params of
+        [single] -> do
+            unless (hasTarget || mayBecomeMultipleArgs single) $
+                handler token
+        _ -> return ()
+  where
+    args = getAllFlags token
+    params = map fst $ filter (\(_,x) -> x == "") args
+    hasTarget =
+        any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
+            map snd args
+
+prop_checkMvArguments1 = verify    checkMvArguments "mv 'foo bar'"
+prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
+prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
+prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
+prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
+prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
+prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
+prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
+prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
+checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
+  where
+    f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
+
+checkCpArguments = CommandCheck (Basename "cp") $ missingDestination f
+  where
+    f t = err (getId t) 2225 "This cp has no destination. Check the arguments."
+
+checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
+  where
+    f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
+
+
 return []
 runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])