Add windows-shell
setting (#1198)
This commit is contained in:
parent
ccc54fb8d9
commit
c24a194602
15
README.md
15
README.md
@ -671,9 +671,22 @@ foo:
|
|||||||
|
|
||||||
`just` passes the command to be executed as an argument. Many shells will need an additional flag, often `-c`, to make them evaluate the first argument.
|
`just` passes the command to be executed as an argument. Many shells will need an additional flag, often `-c`, to make them evaluate the first argument.
|
||||||
|
|
||||||
|
##### Windows Shell
|
||||||
|
|
||||||
|
`just` uses `sh` on Windows by default. To use a different shell on Windows, use `windows-shell`:
|
||||||
|
|
||||||
|
```make
|
||||||
|
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||||
|
|
||||||
|
hello:
|
||||||
|
Write-Host "Hello, world!"
|
||||||
|
```
|
||||||
|
|
||||||
##### Windows PowerShell
|
##### Windows PowerShell
|
||||||
|
|
||||||
`just` uses `sh` on Windows by default. To use PowerShell instead, set `windows-powershell` to true.
|
*`set windows-powershell` uses the legacy `powershell.exe` binary, and is no longer recommended. See the `windows-shell` setting above for a more flexible way to control which shell is used on Windows.*
|
||||||
|
|
||||||
|
`just` uses `sh` on Windows by default. To use `powershell.exe` instead, set `windows-powershell` to true.
|
||||||
|
|
||||||
```make
|
```make
|
||||||
set windows-powershell := true
|
set windows-powershell := true
|
||||||
|
@ -57,12 +57,14 @@ impl<'src> Analyzer<'src> {
|
|||||||
settings.positional_arguments = positional_arguments;
|
settings.positional_arguments = positional_arguments;
|
||||||
}
|
}
|
||||||
Setting::Shell(shell) => {
|
Setting::Shell(shell) => {
|
||||||
assert!(settings.shell.is_none());
|
|
||||||
settings.shell = Some(shell);
|
settings.shell = Some(shell);
|
||||||
}
|
}
|
||||||
Setting::WindowsPowerShell(windows_powershell) => {
|
Setting::WindowsPowerShell(windows_powershell) => {
|
||||||
settings.windows_powershell = windows_powershell;
|
settings.windows_powershell = windows_powershell;
|
||||||
}
|
}
|
||||||
|
Setting::WindowsShell(windows_shell) => {
|
||||||
|
settings.windows_shell = Some(windows_shell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ pub(crate) use ::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
pub(crate) use crate::{completions, config, config_error, keyed, setting};
|
pub(crate) use crate::{completions, config, config_error, keyed};
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent};
|
pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent};
|
||||||
@ -64,11 +64,11 @@ pub(crate) use crate::{
|
|||||||
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
||||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
||||||
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind,
|
settings::Settings, shebang::Shebang, shell::Shell, show_whitespace::ShowWhitespace,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
|
||||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
|
||||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
|
||||||
verbosity::Verbosity, warning::Warning,
|
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
@ -15,6 +15,7 @@ pub(crate) enum Keyword {
|
|||||||
Shell,
|
Shell,
|
||||||
True,
|
True,
|
||||||
WindowsPowershell,
|
WindowsPowershell,
|
||||||
|
WindowsShell,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keyword {
|
impl Keyword {
|
||||||
|
@ -106,6 +106,7 @@ mod set;
|
|||||||
mod setting;
|
mod setting;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
|
mod shell;
|
||||||
mod show_whitespace;
|
mod show_whitespace;
|
||||||
mod string_kind;
|
mod string_kind;
|
||||||
mod string_literal;
|
mod string_literal;
|
||||||
|
15
src/node.rs
15
src/node.rs
@ -215,20 +215,19 @@ impl<'src> Node<'src> for Fragment<'src> {
|
|||||||
|
|
||||||
impl<'src> Node<'src> for Set<'src> {
|
impl<'src> Node<'src> for Set<'src> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
use Setting::*;
|
|
||||||
|
|
||||||
let mut set = Tree::atom(Keyword::Set.lexeme());
|
let mut set = Tree::atom(Keyword::Set.lexeme());
|
||||||
set.push_mut(self.name.lexeme().replace('-', "_"));
|
set.push_mut(self.name.lexeme().replace('-', "_"));
|
||||||
|
|
||||||
match &self.value {
|
match &self.value {
|
||||||
AllowDuplicateRecipes(value)
|
Setting::AllowDuplicateRecipes(value)
|
||||||
| DotenvLoad(value)
|
| Setting::DotenvLoad(value)
|
||||||
| Export(value)
|
| Setting::Export(value)
|
||||||
| PositionalArguments(value)
|
| Setting::PositionalArguments(value)
|
||||||
| WindowsPowerShell(value) => {
|
| Setting::WindowsPowerShell(value) => {
|
||||||
set.push_mut(value.to_string());
|
set.push_mut(value.to_string());
|
||||||
}
|
}
|
||||||
Shell(setting::Shell { command, arguments }) => {
|
Setting::Shell(Shell { command, arguments })
|
||||||
|
| Setting::WindowsShell(Shell { command, arguments }) => {
|
||||||
set.push_mut(Tree::string(&command.cooked));
|
set.push_mut(Tree::string(&command.cooked));
|
||||||
for argument in arguments {
|
for argument in arguments {
|
||||||
set.push_mut(Tree::string(&argument.cooked));
|
set.push_mut(Tree::string(&argument.cooked));
|
||||||
|
@ -764,26 +764,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
self.expect(ColonEquals)?;
|
self.expect(ColonEquals)?;
|
||||||
|
|
||||||
if name.lexeme() == Keyword::Shell.lexeme() {
|
if name.lexeme() == Keyword::Shell.lexeme() {
|
||||||
self.expect(BracketL)?;
|
|
||||||
|
|
||||||
let command = self.parse_string_literal()?;
|
|
||||||
|
|
||||||
let mut arguments = Vec::new();
|
|
||||||
|
|
||||||
if self.accepted(Comma)? {
|
|
||||||
while !self.next_is(BracketR) {
|
|
||||||
arguments.push(self.parse_string_literal()?);
|
|
||||||
|
|
||||||
if !self.accepted(Comma)? {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.expect(BracketR)?;
|
|
||||||
|
|
||||||
Ok(Set {
|
Ok(Set {
|
||||||
value: Setting::Shell(setting::Shell { arguments, command }),
|
value: Setting::Shell(self.parse_shell()?),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} else if name.lexeme() == Keyword::WindowsShell.lexeme() {
|
||||||
|
Ok(Set {
|
||||||
|
value: Setting::WindowsShell(self.parse_shell()?),
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -792,6 +779,29 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a shell setting value
|
||||||
|
fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> {
|
||||||
|
self.expect(BracketL)?;
|
||||||
|
|
||||||
|
let command = self.parse_string_literal()?;
|
||||||
|
|
||||||
|
let mut arguments = Vec::new();
|
||||||
|
|
||||||
|
if self.accepted(Comma)? {
|
||||||
|
while !self.next_is(BracketR) {
|
||||||
|
arguments.push(self.parse_string_literal()?);
|
||||||
|
|
||||||
|
if !self.accepted(Comma)? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.expect(BracketR)?;
|
||||||
|
|
||||||
|
Ok(Shell { arguments, command })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -8,12 +8,7 @@ pub(crate) enum Setting<'src> {
|
|||||||
PositionalArguments(bool),
|
PositionalArguments(bool),
|
||||||
Shell(Shell<'src>),
|
Shell(Shell<'src>),
|
||||||
WindowsPowerShell(bool),
|
WindowsPowerShell(bool),
|
||||||
}
|
WindowsShell(Shell<'src>),
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
|
||||||
pub(crate) struct Shell<'src> {
|
|
||||||
pub(crate) arguments: Vec<StringLiteral<'src>>,
|
|
||||||
pub(crate) command: StringLiteral<'src>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Setting<'src> {
|
impl<'src> Display for Setting<'src> {
|
||||||
@ -24,19 +19,7 @@ impl<'src> Display for Setting<'src> {
|
|||||||
| Setting::Export(value)
|
| Setting::Export(value)
|
||||||
| Setting::PositionalArguments(value)
|
| Setting::PositionalArguments(value)
|
||||||
| Setting::WindowsPowerShell(value) => write!(f, "{}", value),
|
| Setting::WindowsPowerShell(value) => write!(f, "{}", value),
|
||||||
Setting::Shell(shell) => write!(f, "{}", shell),
|
Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{}", shell),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Shell<'src> {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
|
||||||
write!(f, "[{}", self.command)?;
|
|
||||||
|
|
||||||
for argument in &self.arguments {
|
|
||||||
write!(f, ", {}", argument)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(f, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,8 +11,9 @@ pub(crate) struct Settings<'src> {
|
|||||||
pub(crate) dotenv_load: Option<bool>,
|
pub(crate) dotenv_load: Option<bool>,
|
||||||
pub(crate) export: bool,
|
pub(crate) export: bool,
|
||||||
pub(crate) positional_arguments: bool,
|
pub(crate) positional_arguments: bool,
|
||||||
pub(crate) shell: Option<setting::Shell<'src>>,
|
pub(crate) shell: Option<Shell<'src>>,
|
||||||
pub(crate) windows_powershell: bool,
|
pub(crate) windows_powershell: bool,
|
||||||
|
pub(crate) windows_shell: Option<Shell<'src>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Settings<'src> {
|
impl<'src> Settings<'src> {
|
||||||
@ -24,6 +25,7 @@ impl<'src> Settings<'src> {
|
|||||||
positional_arguments: false,
|
positional_arguments: false,
|
||||||
shell: None,
|
shell: None,
|
||||||
windows_powershell: false,
|
windows_powershell: false,
|
||||||
|
windows_shell: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +44,8 @@ impl<'src> Settings<'src> {
|
|||||||
shell.command.cooked.as_ref()
|
shell.command.cooked.as_ref()
|
||||||
} else if let Some(shell) = &config.shell {
|
} else if let Some(shell) = &config.shell {
|
||||||
shell
|
shell
|
||||||
|
} else if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) {
|
||||||
|
shell.command.cooked.as_ref()
|
||||||
} else if cfg!(windows) && self.windows_powershell {
|
} else if cfg!(windows) && self.windows_powershell {
|
||||||
WINDOWS_POWERSHELL_SHELL
|
WINDOWS_POWERSHELL_SHELL
|
||||||
} else {
|
} else {
|
||||||
@ -60,6 +64,12 @@ impl<'src> Settings<'src> {
|
|||||||
.collect()
|
.collect()
|
||||||
} else if let Some(shell_args) = &config.shell_args {
|
} else if let Some(shell_args) = &config.shell_args {
|
||||||
shell_args.iter().map(String::as_ref).collect()
|
shell_args.iter().map(String::as_ref).collect()
|
||||||
|
} else if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) {
|
||||||
|
shell
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.map(|argument| argument.cooked.as_ref())
|
||||||
|
.collect()
|
||||||
} else if cfg!(windows) && self.windows_powershell {
|
} else if cfg!(windows) && self.windows_powershell {
|
||||||
WINDOWS_POWERSHELL_ARGS.to_vec()
|
WINDOWS_POWERSHELL_ARGS.to_vec()
|
||||||
} else {
|
} else {
|
||||||
@ -70,8 +80,6 @@ impl<'src> Settings<'src> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::setting::Shell;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
19
src/shell.rs
Normal file
19
src/shell.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub(crate) struct Shell<'src> {
|
||||||
|
pub(crate) arguments: Vec<StringLiteral<'src>>,
|
||||||
|
pub(crate) command: StringLiteral<'src>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Display for Shell<'src> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
|
write!(f, "[{}", self.command)?;
|
||||||
|
|
||||||
|
for argument in &self.arguments {
|
||||||
|
write!(f, ", {}", argument)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "]")
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ fn alias() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -73,6 +74,7 @@ fn assignment() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -114,6 +116,7 @@ fn body() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -165,6 +168,7 @@ fn dependencies() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -253,6 +257,7 @@ fn dependency_argument() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -305,6 +310,7 @@ fn duplicate_recipes() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -339,6 +345,7 @@ fn doc_comment() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -361,6 +368,7 @@ fn empty_justfile() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -492,6 +500,7 @@ fn parameters() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -562,6 +571,7 @@ fn priors() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -596,6 +606,7 @@ fn private() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -630,6 +641,7 @@ fn quiet() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -685,6 +697,7 @@ fn settings() {
|
|||||||
"command": "a",
|
"command": "a",
|
||||||
},
|
},
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -722,6 +735,7 @@ fn shebang() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
@ -756,6 +770,7 @@ fn simple() {
|
|||||||
"positional_arguments": false,
|
"positional_arguments": false,
|
||||||
"shell": null,
|
"shell": null,
|
||||||
"windows_powershell": false,
|
"windows_powershell": false,
|
||||||
|
"windows_shell": null,
|
||||||
},
|
},
|
||||||
"warnings": [],
|
"warnings": [],
|
||||||
}),
|
}),
|
||||||
|
@ -45,4 +45,6 @@ mod tempdir;
|
|||||||
mod undefined_variables;
|
mod undefined_variables;
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
mod windows_powershell;
|
mod windows_powershell;
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
mod windows_shell;
|
||||||
mod working_directory;
|
mod working_directory;
|
||||||
|
18
tests/windows_shell.rs
Normal file
18
tests/windows_shell.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_shell_setting() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
r#"
|
||||||
|
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||||
|
|
||||||
|
foo:
|
||||||
|
Write-Output bar
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.shell(false)
|
||||||
|
.stdout("bar\r\n")
|
||||||
|
.stderr("Write-Output bar\n")
|
||||||
|
.run();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user