packages feed

swiss-army-knife (empty) → 1.1.0.4

raw patch · 19 files changed

Files

+ CHANGELOG.md view
@@ -0,0 +1,51 @@+# 1.1.0.4 - 2026-07-01++- Adjusted cabal version to be lower for better coverage.+- GHC2021 for better coverage++## Added++- Cross-distro `sak kernel` command+  - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!<sub>OS</sub>, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux+  - Detects running kernel via `uname -r`+  - Shows installed kernels using the native package manager (`pacman`, `dpkg-query`, or `rpm`)+  - Graceful handling when optional kernel packages (e.g. `linux-lts`) are not installed++## Changed++- `Knives.Kernel` module is now fully distribution-agnostic+- Cleaner output formatting with proper newlines+- More robust command execution (no crashes on missing packages)++## Fixed++- Trailing newlines from `readProcess`+- Ambiguous exception type error on GHC 9.14++# 1.0.0.1 and earlier++## Versions++- 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the "-hs" from the name. AUR will pull from Hackage instead. of GitHub.++- 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.++  - Changelog.org removed. Basically a duplication of Versions here.+  - version 1.0.0.0 was stillborn. Installation issues fixed++- 0.1.0.4 - fix for install issues with 0.1.0.3++- 0.1.0.3 - 2 new feature, internally invokes sudo, adopted "standard" for 4 number versioning X.Y.Z.W:++  | \#  | Description                                   |+  |-----|-----------------------------------------------|+  | X   | Major version (breaking changes)              |+  | Y   | Minor version (new features)                  |+  | Z   | Patch version (bug fixes)                     |+  | W   | Build or revision number (specific iteration) |++  So from now on, this will be applied.++- 0.0.1.1 - new feature added++- 0.0.0.2 - bug fixes
+ CHANGELOG.org view
@@ -0,0 +1,45 @@+#+TITLE: Changelog for swiss-army-knife+#+AUTHOR: Fred Mitchell+#+EMAIL: fred.mitchell@atomlogik.de++* 1.1.0.4 - 2026-07-01+  + Adjusted cabal version to be lower for better coverage.+  + GHC2021 for better coverage+** Added+- Cross-distro =sak kernel= command+  - Now works on Arch, Debian, Ubuntu, Linux Mint, Pop!_OS, Fedora, RHEL, CentOS, Rocky Linux, and AlmaLinux+  - Detects running kernel via =uname -r=+  - Shows installed kernels using the native package manager (=pacman=, =dpkg-query=, or =rpm=)+  - Graceful handling when optional kernel packages (e.g. =linux-lts=) are not installed++** Changed+- =Knives.Kernel= module is now fully distribution-agnostic+- Cleaner output formatting with proper newlines+- More robust command execution (no crashes on missing packages)++** Fixed+- Trailing newlines from =readProcess=+- Ambiguous exception type error on GHC 9.14++* 1.0.0.1 and earlier+** Versions+   + 1.1.0.0 - Hackage release, renamed to swiss-army-knife (dropping the+                "-hs" from the name. AUR will pull from Hackage instead.+                of GitHub.+   + 1.0.0.1 - enable and disable daemons, wg -l list has an extra field.+              - Changelog.org removed. Basically a duplication of Versions here.+              - version 1.0.0.0 was stillborn. Installation issues fixed+   + 0.1.0.4 - fix for install issues with 0.1.0.3+   + 0.1.0.3 - 2 new feature, internally invokes sudo,+      adopted "standard" for 4 number versioning X.Y.Z.W:++      | # | Description                                   |+      |---+-----------------------------------------------|+      | X | Major version (breaking changes)              |+      | Y | Minor version (new features)                  |+      | Z | Patch version (bug fixes)                     |+      | W | Build or revision number (specific iteration) |++      So from now on, this will be applied.+   + 0.0.1.1 - new feature added+   + 0.0.0.2 - bug fixes
+ LICENSE view
@@ -0,0 +1,28 @@+Copyright Fred Mitchell (c) 2024++All rights reserved.++Permission is hereby granted, free of charge, to any+person obtaining a copy of this software and associated+documentation files (the "Software"), to deal in the+Software without restriction, including without+limitation the rights to use, copy, modify, merge,+publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the+Software is furnished to do so, subject to the+following conditions:++The above copyright notice and this permission notice+shall be included in all copies or substantial portions+of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.+
+ README.org view
@@ -0,0 +1,79 @@+* swiss-army-knife :TOC_7_gh:+  - [[#created-by][Created By]]+  - [[#introduction][Introduction]]+  - [[#documentation][Documentation]]+    - [[#sak---help][sak --help]]+    - [[#sak-extip][sak extip]]+    - [[#sak-kernel][sak kernel]]+    - [[#sak-sleep][sak sleep]]+    - [[#sak-zfscheck][sak zfscheck]]+    - [[#sak-wg][sak wg]]+    - [[#sak-nm][sak nm]]+    - [[#sak-sn][sak sn]]+  - [[#examples][Examples]]++** Created By+   Fred Mitchell & Atomlogik+      +** Introduction+   Swiss-Army-Knife is a collection of useful+   tools that you can do quickly.+   +   This is now in Hackage and the AUR. So in theory,+   it can run "anywhere", though I have not had the time to test this,+   so I am open to comments and pull requests. In particular, it is +   gauranteed to run on Arch, and I do have some support for other+   distros, and that will grow. But I have not had the time to fully+   test other distros.++   So some knives will run "anywhere", but some that call out to+   the system for system-specific information may have issues.+   As I find out about them, I will fix them as quickly as I can.++** Documentation+   These are the current "knives" implemented. More to come. Open+   to suggestions.+   + Note that you no longer have to prepend "sudo" to do system-level+      operations.+*** sak --help+    Will list all the current commands.+*** sak extip+    Will tell you what your external IP is for your connection,+    either in IPv4 or IPv6 Useful if you use a VPN+*** sak kernel+    Will tell you the version of the installed kernel as well+    as the running version. Support is in for Arch, Debian, and+    RedHat, but only Arch has been tested.+*** sak sleep+    Will put your system in standby mode.+*** sak zfscheck+    If you use the zfs from archzfs, sometimes it +    does not keep up with the latest kernel release.+    Check to see the latest kernel zfs supports before+    you do a system upgrade. Other distros will probably+    not have the zfs sync issues that Arch does.+*** sak wg+    If you use WireGuard for your VPN, manage it from+    here. List your installed WireGuard options,+    and swap among them. you can start, stop, and list your+    WireGuard VPNs.++    Now supports enabling and disabling of the underlying daemon.+*** sak nm+    Manage NetworkManager+    Now supports enabling and disabling of the underlying daemon.++*** sak sn+    Manage systemd-networkd+    Now supports enabling and disabling of the underlying daemon.++** Examples+   | Command   | Description                                                                                                        |+   |-----------+--------------------------------------------------------------------------------------------------------------------|+   | sak extip | Will list your external IP and location information                                                                |+   | sak wg -l | (If you are using wg_quick) will list all of your available WG vpns, as well as their active and enabled statuses. |+   | sak wg -d | Will deactivate (but not disable) all WireGuard VPNs.                                                            |+   | sak wg -D | Will disable (but not deactivate) all WireGuard VPNs.                                                              |+   | sak sn -A | Will enable and start systemd-networkd.                                                                            |+   | sak nm -D | Will disable and stop NetworkManager.                                                                              |+   |-----------+--------------------------------------------------------------------------------------------------------------------|
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/SwissArmyKnife.hs view
@@ -0,0 +1,25 @@+-- main for Swiss Army Knife, cli.++module Main where++import Options.Applicative+import Data.Semigroup ((<>))+import CommandLine+import Knives++main :: IO ()+main = do+  (globalOpts, cmd) <- execParser $ info (opts <**> helper)+    ( fullDesc+      <> progDesc "Many useful utilities, such as getting your external IP address, installed kernel, controlling your WireGuard, etc."+      <> header "Swiss Army Knife -- Many useful functions for the hacker in all of us." )+  +  case cmd of+    ExtIP extipOpts     -> knifeExtIP     extipOpts+    Kernel kernelOpts   -> knifeKernel    kernelOpts+    Sleep sleepOpts     -> knifeSleep     sleepOpts +    Version versionOpts -> knifeVersion   versionOpts +    ZfsCheck zfsOpts    -> knifeZfsCheck  zfsOpts+    WireGuard wgOpts    -> knifeWireGuard wgOpts+    NetMan nmOpts       -> knifeNetMan    nmOpts+    SysNet snOpts       -> knifeSysNet    snOpts
+ lib/CommandLine.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}++module CommandLine where++import Options.Applicative++-- Define data types for our commands and options+data Command+  = ExtIP     ExtIPOptions+  | Version   VersionOptions+  | Kernel    KernelOptions+  | Sleep     SleepOptions+  | ZfsCheck  ZfsCheckOptions+  | WireGuard WireGuardOptions+  | NetMan    NetManOptions+  | SysNet    SysNetOptions+  deriving Show ++data ExtIPOptions = ExtIPOptions+  { nolocation :: Bool+  , ipv6       :: Bool+  } deriving Show++data KernelOptions = KernelOptions+  { justVersion :: Bool } deriving Show++data SleepOptions = SleepOptions+  { secondsToSleep :: Maybe Int } deriving Show++data VersionOptions = VersionOptions Bool deriving Show++data ZfsCheckOptions = ZfsCheckOptions+  { full :: Bool } deriving Show++data WireGuardOptions = WireGuardOptions+  { listWGs      :: Bool+  , activateWG   :: Maybe String+  , enableWG     :: Maybe String+  , deactivateWG :: Bool+  , disableWG    :: Bool+  , reactivateWG :: Bool+  } deriving Show++data NetManOptions = NetManOptions+  { activateNM   :: Bool+  , enableNM     :: Bool+  , deactivateNM :: Bool+  , disableNM    :: Bool+  , reactivateNM :: Bool+  } deriving Show++data SysNetOptions = SysNetOptions+  { activateSN   :: Bool+  , enableSN     :: Bool+  , deactivateSN :: Bool+  , disableSN    :: Bool+  , reactivateSN :: Bool+  } deriving Show++data GlobalOptions = GlobalOptions+  { verbose :: Bool } deriving Show++globalOptionsParser :: Parser GlobalOptions+globalOptionsParser = GlobalOptions+  <$> switch ( long     "verbose"+               <> short 'v'+               <> help  "Enable verbose mode" )++extipOptionsParser :: Parser ExtIPOptions+extipOptionsParser = ExtIPOptions+  <$> switch ( long "nolocation"+               <> short 'n'+               <> help "Don't display location information" )+  <*> switch ( long "ipv6"+               <> short 'i'+               <> help "Display the IPv6 address" )++kernelOptionsParser :: Parser KernelOptions+kernelOptionsParser = KernelOptions+  <$> switch ( long "justversion"+               <> short 'j'+               <> help "Just display the numerical version number, no adjoining text." )++-- version has no options+versionOptionsParser :: Parser VersionOptions+versionOptionsParser = VersionOptions+  <$> switch ( long "noop" )+  +sleepOptionsParser :: Parser SleepOptions+sleepOptionsParser = SleepOptions+  <$> optional (option auto ( long "wait"+                              <> short 'w'+                              <> metavar "SECONDS"+                              <> help "Seconds to wait before going to sleep"))++zfscheckOptionsParser :: Parser ZfsCheckOptions+zfscheckOptionsParser = ZfsCheckOptions+  <$> switch+  ( long "full"+    <> short 'f'+    <> help "Check all kernel versions" )++wgOptionsParser :: Parser WireGuardOptions+wgOptionsParser = WireGuardOptions+  <$> switch   ( long "list"+                 <> short 'l'+                 <> help "List all installed WireGuardG VPNs")+  <*> optional (strOption ( long "activate"+                              <> short 'a'+                              <> metavar "WGVPN"+                              <> help "WireGuard VPN to activate"))+  <*> optional (strOption ( long "enable"+                              <> short 'A'+                              <> metavar "WGVPN"+                              <> help "WireGuard VPN to enable and activate"))+  <*> switch   ( long "deactivate"+                 <> short 'd'+                 <> help "Deactivate (but not disable) all WireGuard VPNs")+  <*> switch   ( long "disable"+                 <> short 'D'+                 <> help "Disable (but not deactivate) all WireGuard VPNs")+  <*> switch   ( long "reactivate"+                 <> short 'r'+                 <> help "Reactivate currently active WireGuardG VPNs")++nmOptionsParser :: Parser NetManOptions+nmOptionsParser = NetManOptions+  <$> switch   ( long "activate"+                 <> short 'a'+                 <> help "Activate (start) NetworkManager")+  <*> switch   ( long "enable"+                 <> short 'A'+                 <> help "Enable and Activate (start) NetworkManager")+  <*> switch   ( long "deactivate"+                 <> short 'd'+                 <> help "Deactivate (stop) NetworkManager")+  <*> switch   ( long "disable"+                 <> short 'D'+                 <> help "Disable and Deactivate (stop) NetworkManager")+  <*> switch   ( long "reactivate"+                 <> short 'r'+                 <> help "Reactivate (restart) NetworkManager")++snOptionsParser :: Parser SysNetOptions+snOptionsParser = SysNetOptions+  <$> switch   ( long "activate"+                 <> short 'a'+                 <> help "Activate (start) systemd-networkd socket and service")+  <*> switch   ( long "enable"+                 <> short 'A'+                 <> help "Enable and Activate (start) systemd-networkd socket and service")+  <*> switch   ( long "deactivate"+                 <> short 'd'+                 <> help "Deactivate (stop) systemd-networkd socket and service")+  <*> switch   ( long "disable"+                 <> short 'D'+                 <> help "Disable and Deactivate (stop) systemd-networkd socket and service")+  <*> switch   ( long "reactivate"+                 <> short 'r'+                 <> help "Reactivate (restart) systemd-networkd socket and service")+++-- Combine the subcommand parsers+commandParser :: Parser Command+commandParser = subparser+  (    command "extip"    (info (ExtIP     <$> extipOptionsParser)    (progDesc "Display external IP address"))+    <> command "kernel"   (info (Kernel    <$> kernelOptionsParser)   (progDesc "Display kernel information, both installed and currently running"))+    <> command "sleep"    (info (Sleep     <$> sleepOptionsParser)    (progDesc "Put the machine to sleep"))+    <> command "version"  (info (Version   <$> versionOptionsParser)  (progDesc "Version of Swiss Army Knife"))+    <> command "zfscheck" (info (ZfsCheck  <$> zfscheckOptionsParser) (progDesc "Check zfs availibility for the current kernel"))+    <> command "wg"       (info (WireGuard <$> wgOptionsParser)       (progDesc "Manage WireGuard VPNs"))+    <> command "nm"       (info (NetMan    <$> nmOptionsParser)       (progDesc "Manage NetworkManager"))+    <> command "sn"       (info (SysNet    <$> snOptionsParser)       (progDesc "Manage systemd-networkd"))+  )++-- Combine global options with the command parser+opts :: Parser (GlobalOptions, Command)+opts = (,) <$> globalOptionsParser <*> commandParser
+ lib/Knives.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives+    ( knifeKernel+    , knifeExtIP+    , knifeSleep+    , knifeVersion+    , knifeZfsCheck+    , knifeWireGuard+    , knifeNetMan+    , knifeSysNet+    ) where+++import Knives.ExtIP+import Knives.Kernel+import Knives.Sleep+import Knives.Version+import Knives.WireGuard+import Knives.ZfsCheck+import Knives.NetMan+import Knives.SysNet+
+ lib/Knives/ExtIP.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.ExtIP where++import CommandLine+import Network.HTTP.Simple+import Data.Aeson+import Data.Bool (bool)+import Control.Monad ((>=>))++data RExtIP = RExtIP+  { ip :: String } deriving Show++instance FromJSON RExtIP where+  parseJSON = withObject "RExtIP" $ \v -> RExtIP+    <$> v .: "ip"++data RLoc = RLoc+  {status      :: String+  , country    :: String+  , countryCode:: String+  , region     :: String+  , regionName :: String+  , city       :: String+  , zip        :: String+  , lat        :: Float+  , lon        :: Float+  , timezone   :: String+  , isp        :: String+  , org        :: String+  , as         :: String+  , query      :: String+  } deriving Show++instance FromJSON RLoc where+  parseJSON = withObject "RLoc" $ \v -> RLoc+    <$> v .: "status"+    <*> v .: "country"+    <*> v .: "countryCode"+    <*> v .: "region"+    <*> v .: "regionName"+    <*> v .: "city"+    <*> v .: "zip"+    <*> v .: "lat"+    <*> v .: "lon"+    <*> v .: "timezone"+    <*> v .: "isp"+    <*> v .: "org"+    <*> v .: "as"+    <*> v .: "query"+  +knifeExtIP :: ExtIPOptions -> IO ()+knifeExtIP optsE = do+  res <- (parseRequest >=> httpLBS) $ bool url_ipv4 url_ipv6 $ ipv6 optsE+  putStrLn $ ipaddr res+  if (not $ nolocation optsE)+    then do+      loc <- location+      putStrLn $ loc+    else return ()++  where+    url_ipv4 = "https://ipv4.jsonip.com"+    url_ipv6 = "https://ipv6.jsonip.com"+    url_geo  = "http://ip-api.com/json/"+    ipaddr res = do+      let ipresult = eitherDecode $ getResponseBody res :: Either String RExtIP+      case ipresult of+        Left err -> do+          "Failed to obtain IP: " ++ err+        Right extIP -> do+          ip extIP++    location :: IO String+    location = do+      request <- parseRequest url_geo+      res <- httpLBS request+      let locres = eitherDecode $ getResponseBody res :: Either String RLoc+      case locres of+        Left err -> do+          return $ "Failed to obtain location: " ++ err+        Right loc -> do+          let str_result = "Location: "+                ++ city loc ++ ", "+                ++ country loc+                ++ " (" ++ countryCode loc ++ ")\n"+                ++ "Timezone: " ++ timezone loc ++ "\n"+                +          return str_result
+ lib/Knives/Kernel.hs view
@@ -0,0 +1,143 @@+module Knives.Kernel+  ( knifeKernel+  , KernelOptions(..)+  ) where++import CommandLine (KernelOptions(..))+import System.Process (readProcess)+import Control.Exception (try, IOException)+import Data.Char (isSpace)+import Data.List (dropWhileEnd, isInfixOf, isPrefixOf)+import System.Directory (doesFileExist)+import Data.Maybe (catMaybes)++-- | Trim leading and trailing whitespace/newlines+trim :: String -> String+trim = dropWhileEnd isSpace . dropWhile isSpace++-- | Safely run a command. Returns Nothing if it fails or command not found.+tryReadProcess :: String -> [String] -> IO (Maybe String)+tryReadProcess cmd args = do+  result <- try @IOException (readProcess cmd args "")+  pure $ either (const Nothing) (Just . trim) result++-- | Detect Debian-based systems (Debian, Ubuntu, Mint, Pop!_OS, Kali, etc.)+isDebianBased :: IO Bool+isDebianBased = do+  hasDebian <- doesFileExist "/etc/debian_version"+  if hasDebian+    then pure True+    else checkOsRelease+  where+    checkOsRelease = do+      hasOs <- doesFileExist "/etc/os-release"+      if not hasOs then pure False else do+        content <- readFile "/etc/os-release"+        let debianLike = ["debian", "ubuntu", "linuxmint", "pop", "kali", "elementary", "zorin"]+        pure $ any (\idName ->+                      ("ID="        ++ idName) `isInfixOf` content ||+                      ("ID_LIKE="   ++ idName) `isInfixOf` content+                   ) debianLike++-- | Detect Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, Oracle, etc.)+isRedHatBased :: IO Bool+isRedHatBased = do+  hasRedhat <- doesFileExist "/etc/redhat-release"+  if hasRedhat+    then pure True+    else checkOsRelease+  where+    checkOsRelease = do+      hasOs <- doesFileExist "/etc/os-release"+      if not hasOs then pure False else do+        content <- readFile "/etc/os-release"+        let redhatLike = ["rhel", "centos", "fedora", "rocky", "almalinux", "ol", "scientific"]+        pure $ any (\idName ->+                      ("ID="      ++ idName) `isInfixOf` content ||+                      ("ID_LIKE=" ++ idName) `isInfixOf` content+                   ) redhatLike++-- | Get the currently running kernel (works on all Linux distros)+getRunningKernel :: IO String+getRunningKernel = do+  mVer <- tryReadProcess "uname" ["-r"]+  pure $ maybe "unknown" id mVer++-- | Get installed kernel packages (distro-aware)+getInstalledKernels :: IO [String]+getInstalledKernels = do+  debian <- isDebianBased+  if debian+    then getDebianKernels+    else do+      redhat <- isRedHatBased+      if redhat+        then getRedHatKernels+        else getArchKernels++-- | Debian / Ubuntu / Mint style+getDebianKernels :: IO [String]+getDebianKernels = do+  mOut <- tryReadProcess "dpkg-query"+            ["-W", "-f=${Package} ${Version}\n", "linux-image-*"]+  case mOut of+    Just out | not (null out) ->+      pure $ filter (not . null) $ lines out+    _ -> do+      -- Fallback to dpkg -l+      mOut2 <- tryReadProcess "dpkg" ["-l"]+      case mOut2 of+        Just out2 ->+          pure $ take 8+               $ map (unwords . take 4 . words)+               $ filter ("linux-image" `isInfixOf`) (lines out2)+        Nothing -> pure []++-- | Red Hat family (RHEL, CentOS, Fedora, Rocky, AlmaLinux, etc.)+getRedHatKernels :: IO [String]+getRedHatKernels = do+  mOut <- tryReadProcess "rpm"+            [ "-qa"+            , "--qf"+            , "%{NAME}-%{VERSION}-%{RELEASE}\n"+            , "kernel*"+            ]+  case mOut of+    Just out | not (null (trim out)) ->+      pure $ take 8+           $ filter (\pkg ->+               "kernel" `isPrefixOf` pkg &&+               not ("headers"   `isInfixOf` pkg) &&+               not ("devel"     `isInfixOf` pkg) &&+               not ("doc"       `isInfixOf` pkg) &&+               not ("tools"     `isInfixOf` pkg)+             )+           $ lines (trim out)+    _ -> pure []++-- | Arch Linux style+getArchKernels :: IO [String]+getArchKernels = do+  mLinux <- tryReadProcess "pacman" ["-Q", "linux"]+  mLts   <- tryReadProcess "pacman" ["-Q", "linux-lts"]+  mZen   <- tryReadProcess "pacman" ["-Q", "linux-zen"]+  mHard  <- tryReadProcess "pacman" ["-Q", "linux-hardened"]+  pure $ catMaybes [mLinux, mLts, mZen, mHard]++-- | Main entry point for the `sak kernel` command+knifeKernel :: KernelOptions -> IO ()+knifeKernel optsK = do+  running <- getRunningKernel++  if justVersion optsK+    then putStrLn running+    else do+      installed <- getInstalledKernels++      putStrLn $ "Running kernel:  " ++ running++      if null installed+        then putStrLn "Installed kernels: (could not determine on this distribution)"+        else do+          putStrLn "Installed kernels:"+          mapM_ (putStrLn . ("  " ++)) installed
+ lib/Knives/NetMan.hs view
@@ -0,0 +1,31 @@+module Knives.NetMan where++import Utils+import CommandLine+import Control.Monad (when)++knifeNetMan :: NetManOptions -> IO ()+knifeNetMan NetManOptions { activateNM+                          , enableNM+                          , deactivateNM+                          , disableNM+                          , reactivateNM}+  | isExclusiveOr [ activateNM+                  , enableNM+                  , deactivateNM+                  , disableNM+                  , reactivateNM] = do+      when activateNM   activate+      when enableNM     enable+      when deactivateNM deactivate+      when disableNM    disable+      when reactivateNM reactivate+      return ()+  | otherwise = putStrLn("You must specify one and only one option for NetMan.")+  where+    service    = "NetworkManager.service"+    activate   = systemctl_ ["start",   service]+    enable     = systemctl_ ["enable", "--now", service]+    deactivate = systemctl_ ["stop",    service]+    disable    = systemctl_ ["disable", "--now", service]+    reactivate = systemctl_ ["restart", service]
+ lib/Knives/Sleep.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.Sleep where++import System.Process+import CommandLine++knifeSleep :: SleepOptions -> IO ()+knifeSleep optsS = do+  putStrLn $ "Put the machine to sleep." ++ show (secondsToSleep optsS)+  case (secondsToSleep optsS) of+    Just secs -> do putStrLn $ "sleep in " ++ show secs ++ " seconds."+                    _n <- readProcess "sleep" [show secs] ""+                    return ()+    Nothing  ->  putStrLn "sleep immediatly"+  _nn <-  readProcess "systemctl" ["suspend", "-i"] ""+  return ()
+ lib/Knives/SysNet.hs view
@@ -0,0 +1,46 @@+module Knives.SysNet where++import Utils+import CommandLine+import Control.Monad (when)++knifeSysNet :: SysNetOptions -> IO ()+knifeSysNet SysNetOptions { activateSN+                          , enableSN+                          , deactivateSN+                          , disableSN+                          , reactivateSN}+  | isExclusiveOr [ activateSN+                  , enableSN+                  , deactivateSN+                  , disableSN+                  , reactivateSN] = do+      when activateSN   activate+      when enableSN     enable+      when deactivateSN deactivate+      when disableSN    disable+      when reactivateSN reactivate+  | otherwise = putStrLn("You must specify one and only one option for SysNet.")+  where+    socket  = "systemd-networkd.socket"+    service = "systemd-networkd.service"+    +    activate   = do+      systemctl_ ["start",   socket]+      systemctl_ ["start",   service]++    enable     = do+      systemctl_ ["start",   socket]+      systemctl_ ["enable", "--now",  service]++    deactivate = do+      systemctl_ ["stop",   socket]+      systemctl_ ["stop",   service]+ +    disable    = do+      systemctl_ ["stop",   socket]+      systemctl_ ["disable", "--now", service]+ +    reactivate = do+      systemctl_ ["restart",   socket]+      systemctl_ ["restart",   service]
+ lib/Knives/Version.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}++module Knives.Version where++import CommandLine+import qualified Paths_swiss_army_knife as SAK+import Data.Version (showVersion)++knifeVersion :: VersionOptions -> IO ()+knifeVersion _ = do+  putStrLn $ "Swiss Army Knife version: " ++ showVersion SAK.version+  return ()
+ lib/Knives/WireGuard.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module Knives.WireGuard where++import Utils (systemctl)+import CommandLine+-- import System.IO+-- import System.Process+import System.Directory (getDirectoryContents)+import System.FilePath (dropExtension, takeExtension)+import Control.Monad (when)+import Data.List++knifeWireGuard :: WireGuardOptions -> IO ()+knifeWireGuard WireGuardOptions { listWGs+                                , activateWG+                                , enableWG+                                , deactivateWG+                                , disableWG+                                , reactivateWG}+  | checkMutualExclusivity = do+      wgs <- wgList+      ss  <- wgStatus wgs+      when listWGs $ putStrLn $ formatStatus ss+      case (activateWG , enableWG , deactivateWG , disableWG , reactivateWG) of+            (Just vpn  , Nothing  , False        , False     , False) -> activate vpn+            (Nothing   , Just vpn , False        , False     , False) -> enable vpn+            (Nothing   , Nothing  , True         , False     , False) -> deactivate+            (Nothing   , Nothing  , False        , True      , False) -> disable+            (Nothing   , Nothing  , False        , False     , True ) -> reactivate+            _                        -> return ()+  | otherwise = putStrLn "Options aside from --list or -l are mutually exclusive"+  where+    wgDir = "/etc/wireguard/"++    wgList :: IO [String]+    wgList = do+      rawlist <- getDirectoryContents wgDir+      let wgconf = filter (\f -> takeExtension f == ".conf") rawlist+      let wgs    = map (\f -> dropExtension f) wgconf+      return wgs+      +    wgStatus :: [String] -> IO [(String, String, String, Bool, Bool)]+    wgStatus wgs = do+      let quick = map (\w -> vpn2wg w) wgs+      let cmd_a   = ["is-active"]  <> quick+      let cmd_e   = ["is-enabled"] <> quick+      stats_a <- systemctl cmd_a+      stats_e <- systemctl cmd_e+      return $ [(wg+                , sact+                , sena+                , sact == "active"+                , sena == "enabled") | (wg, sact, sena) <- zip3 wgs stats_a stats_e ]++    formatStatus :: [(String, String, String, Bool, Bool)] -> String+    formatStatus wss = intercalate "\n" [w <> ": " <> sa <> " " <> se |(w, sa, se, _, _) <- wss]++    checkMutualExclusivity = check [ activateWG /= Nothing+                                   , enableWG   /= Nothing+                                   , deactivateWG+                                   , disableWG+                                   , reactivateWG]++    check bs   = (length $ filter id bs) `elem` [0, 1]+    vpn2wg vpn = "wg-quick@" <> vpn+    +    activate :: String -> IO ()+    activate vpn = do+      _ <- systemctl ["start", vpn2wg vpn]+      return ()++    enable :: String -> IO ()+    enable vpn = do+      _ <- systemctl ["enable", "--now", vpn2wg vpn]+      return ()+    +    deactivate :: IO ()+    deactivate   = do+      awgs <- activeWG+      mapM_ (\w -> systemctl $ ["stop"] <> [vpn2wg w]) awgs+      return ()++    disable :: IO ()+    disable = do+      awgs <- enabledWG+      mapM_ (\w -> systemctl $ ["disable", "--now"] <> [vpn2wg w]) awgs+      return ()+++    reactivate :: IO ()+    reactivate   = do+      awgs <- activeWG+      mapM_ (\w -> systemctl $ ["restart"] <>  [vpn2wg w]) awgs+      return()++    activeWG :: IO [String]+    activeWG = do+      wgl <- wgList+      wgs <- wgStatus wgl+      return [s | (s, _, _, a, _) <- wgs, a]++    enabledWG :: IO [String]+    enabledWG = do+      wgl <- wgList+      wgs <- wgStatus wgl+      return [s | (s, _, _, _, e) <- wgs, e]
+ lib/Knives/ZfsCheck.hs view
@@ -0,0 +1,44 @@++module Knives.ZfsCheck where++import CommandLine+import Data.List.Split (splitOn)+import Data.Char (isSpace)+import Data.List (isInfixOf)+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LBS8+import System.Process+++knifeZfsCheck :: ZfsCheckOptions -> IO ()+knifeZfsCheck _ = do+  putStrLn $ "Check the version of archzfs with repo kernel version so you can determine if an upgrade is possible."++  let url_zfs = "https://raw.githubusercontent.com/openzfs/zfs/master/META"++  repo_version  <- readProcess "pacman" ["-Si", "linux"] "" -- | awk '/^Version/ { print $3 }'" ""+  archzfs_version <- readProcess "curl" ["-sSL", url_zfs] ""+  skernel <- readProcess "uname" ["-r"] ""+  sinstalled <- readProcess "pacman" ["-Q", "linux"] ""++  let repo    = splitAndTrim+                $ LBS8.unpack+                $ head [v | v <- LBS8.lines (LBS8.pack repo_version), (LBS8.pack "Version") `LBS.isPrefixOf` v]+      archzfs = splitAndTrim+                $ LBS8.unpack+                $ head [v | v <- LBS8.lines (LBS8.pack archzfs_version), (LBS8.pack "Linux-Maximum:") `LBS.isPrefixOf` v]+      running = trim skernel+      installed = unwords+                $ filter (not . ("linux" `isInfixOf`))+                $ words+                $ trim sinstalled++  putStrLn $ "\n      repo: " ++ repo+    ++ "\n       zfs: " ++ archzfs+    ++ "\n   running: " ++ running+    ++ "\n installed: " ++ installed++  where+    splitAndTrim line = trim $ last $ splitOn ":" line+    trim = f . f+      where f = reverse . dropWhile isSpace
+ lib/Utils.hs view
@@ -0,0 +1,32 @@+module Utils where++import System.IO+import System.Process+-- import System.Directory (getDirectoryContents)+-- import System.FilePath (dropExtension, takeExtension)++-- WARN: Cannot handle inflinite lists.+isExclusiveOr :: [Bool] -> Bool+isExclusiveOr bs = (length $ filter id bs) == 1++-- WARN: Cannot handle inflinite lists.+isExclusiveOrNone :: [Bool] -> Bool+isExclusiveOrNone bs = (length $ filter id bs) `elem` exorno+  where +   exorno = [0, 1]++systemctl :: [String] -> IO [String]+systemctl parms = systemG $ "sudo" : "systemctl" : parms  +      +systemctl_ :: [String] -> IO ()+systemctl_ parms = do+  _ <- systemG $ "sudo" : "systemctl" : parms+  return ()++systemG :: [String] -> IO [String]+systemG [] = return []+systemG (cmd:parms) = do+  (_, Just hout, _, _) <- createProcess (proc cmd parms) { std_out = CreatePipe }+  out <- hGetContents hout+  return $ lines out+      
+ swiss-army-knife.cabal view
@@ -0,0 +1,91 @@+cabal-version: 2.4+name:           swiss-army-knife+version:        1.1.0.4+synopsis:       A collection of small tools to make life a bit easier for the command-line.+category:       System+description:    Please see the README on GitHub at <https://github.com/flajann2/swiss-army-knife#readme>+homepage:       https://github.com/flajann2/swiss-army-knife#readme+bug-reports:    https://github.com/flajann2/swiss-army-knife/issues+author:         Fred Mitchell+maintainer:     fred.mitchell@atomlogik.de+copyright:      2024-2026 Fred Mitchell & Atomlogik+license:        MIT+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.org+    CHANGELOG.org+    LICENSE+extra-doc-files:+    README.org+    CHANGELOG.org+    CHANGELOG.md+    +tested-with: GHC == 9.14.1++source-repository head+  type: git+  location: https://github.com/flajann2/swiss-army-knife++library+  exposed-modules:+      CommandLine+      Utils+      Knives+      Knives.Version+      Knives.Sleep+      Knives.ExtIP+      Knives.WireGuard+      Knives.ZfsCheck+      Knives.Kernel+      Knives.SysNet+      Knives.NetMan+  autogen-modules:+      Paths_swiss_army_knife+  other-modules:+      Paths_swiss_army_knife+  hs-source-dirs:+      lib+  ghc-options: -Wall+  build-depends:+      aeson                   >= 2.1     && < 2.4+    , base                    >= 4.18    && < 5+    , bytestring              >= 0.11    && < 0.13+    , directory               >= 1.3.8   && < 1.4+    , filepath                >= 1.4.2   && < 1.6+    , http-conduit            >= 2.3.8   && < 2.4+    , optparse-applicative    >= 0.18    && < 0.20+    , process                 >= 1.6.18  && < 1.7+    , resolv                  >= 0.1.2   && < 0.3+    , split                   >= 0.2.5   && < 0.3+  default-language: GHC2021++executable sak+  main-is: SwissArmyKnife.hs+  autogen-modules:+      Paths_swiss_army_knife+  other-modules:+      Paths_swiss_army_knife+  hs-source-dirs:+      app+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base                    >= 4.18    && < 5+    , optparse-applicative    >= 0.18    && < 0.20+    , swiss-army-knife+  default-language: GHC2021++test-suite swiss-army-knife-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  autogen-modules:+      Paths_swiss_army_knife+  other-modules:+      Paths_swiss_army_knife+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base                    >= 4.18    && < 5+    , swiss-army-knife+  default-language: GHC2021
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"